diff options
1916 files changed, 29984 insertions, 13987 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index e5636a13783..42afed54371 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -10,10 +10,10 @@ engines: languages: - ruby - javascript + exclude_paths: + - "lib/api/v3/*" eslint: enabled: true - fixme: - enabled: true rubocop: enabled: true ratings: @@ -35,4 +35,13 @@ exclude_paths: - node_modules/ - spec/ - vendor/ -- lib/api/v3/ +- .yarn-cache/ +- tmp/ +- builds/ +- coverage/ +- public/ +- shared/ +- webpack-report/ +- log/ +- backups/ +- coverage-javascript/ diff --git a/.gitignore b/.gitignore index 89da29fd790..e529e33530a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ eslint-report.html /.yarn-cache /.byebug_history /Vagrantfile +/app/assets/javascripts/locale/**/app.js /backups/* /config/aws.yml /config/database.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b442e48a3d0..790d9a1f72a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -193,6 +193,7 @@ setup-test-env: script: - node --version - yarn install --pure-lockfile --cache-folder .yarn-cache + - bundle exec rake gettext:po_to_json - bundle exec rake gitlab:assets:compile - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' artifacts: @@ -433,6 +434,7 @@ gitlab:assets:compile: NO_COMPRESSION: "true" script: - yarn install --pure-lockfile --production --cache-folder .yarn-cache + - bundle exec rake gettext:po_to_json - bundle exec rake gitlab:assets:compile artifacts: name: webpack-report @@ -441,21 +443,43 @@ gitlab:assets:compile: - webpack-report/ karma: + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-chrome-59.0-node-7.1-postgresql-9.6" stage: test <<: *use-pg <<: *dedicated-runner <<: *except-docs variables: BABEL_ENV: "coverage" + CHROME_LOG_FILE: "chrome_debug.log" script: + - bundle exec rake gettext:po_to_json - bundle exec rake karma coverage: '/^Statements *: (\d+\.\d+%)/' artifacts: name: coverage-javascript expire_in: 31d + when: always paths: + - chrome_debug.log - coverage-javascript/ +codeclimate: + <<: *except-docs + before_script: [] + image: docker:latest + stage: test + variables: + SETUP_DB: "false" + DOCKER_DRIVER: overlay + services: + - docker:dind + script: + - docker pull codeclimate/codeclimate + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json + - sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json + artifacts: + paths: [codeclimate.json] + coverage: stage: post-test services: [] @@ -527,3 +551,9 @@ cache gems: only: - master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ee + +gitlab_git_test: + variables: + SETUP_DB: "false" + script: + - spec/support/prepare-gitlab-git-test-for-commit --check-for-changes diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 9d53a48409a..aec734870d6 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -1,11 +1,18 @@ Please read this! Before opening a new issue, make sure to search for keywords in the issues -filtered by the "regression" or "bug" label: +filtered by the "regression" or "bug" label. + +For the Community Edition issue tracker: - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug +For the Enterprise Edition issue tracker: + +- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=regression +- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug + and verify the issue you're about to submit isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate. diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md index d96c9ad59e0..1278061a410 100644 --- a/.gitlab/issue_templates/Feature Proposal.md +++ b/.gitlab/issue_templates/Feature Proposal.md @@ -3,8 +3,14 @@ Please read this! Before opening a new issue, make sure to search for keywords in the issues filtered by the "feature proposal" label: +For the Community Edition issue tracker: + - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal +For the Enterprise Edition issue tracker: + +- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal + and verify the issue you're about to submit isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate. @@ -21,12 +27,24 @@ Please remove this notice if you're confident your issue isn't a duplicate. ### Documentation blurb -(Write the start of the documentation of this feature here, include: +#### Overview + +What is it? +Why should someone use this feature? +What is the underlying (business) problem? +How do you use this feature? + +#### Use cases + +Who is this for? Provide one or more use cases. + +### Feature checklist -1. Why should someone use it; what's the underlying problem. -2. What is the solution. -3. How does someone use this +Make sure these are completed before closing the issue, +with a link to the relevant commit. -During implementation, this can then be copied and used as a starter for the documentation.) +- [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance) +- [ ] Documentation +- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml) -/label ~"feature proposal" +/label ~"feature proposal"
\ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index 66a40f2cf57..32ec60f540b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -164,6 +164,11 @@ Style/DefWithParentheses: Style/Documentation: Enabled: false +# Multi-line method chaining should be done with leading dots. +Style/DotPosition: + Enabled: true + EnforcedStyle: leading + # This cop checks for uses of double negation (!!) to convert something # to a boolean value. As this is both cryptic and usually redundant, it # should be avoided. @@ -1064,6 +1069,13 @@ RSpec/NotToNot: RSpec/RepeatedDescription: Enabled: false +# Ensure RSpec hook blocks are always multi-line. +RSpec/SingleLineHook: + Enabled: true + Exclude: + - 'spec/factories/*' + - 'spec/requests/api/v3/*' + # Checks for stubbed test subjects. RSpec/SubjectStub: Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e2d9c37479d..5ab4692dd60 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -88,13 +88,6 @@ Security/YAMLLoad: Style/BarePercentLiterals: Enabled: false -# Offense count: 1403 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: leading, trailing -Style/DotPosition: - Enabled: false - # Offense count: 5 # Cop supports --auto-correct. Style/EachWithObject: diff --git a/CHANGELOG.md b/CHANGELOG.md index e5567dc3b39..f372cbf91e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,253 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.3.2 (2017-06-27) + +- API: Fix optional arugments for POST :id/variables. !12474 +- Bump premailer-rails gem to 1.9.7 and its dependencies to prevent network retrieval of assets. + +## 9.3.1 (2017-06-26) + +- Fix reversed breadcrumb order for nested groups. !12322 +- Fix 500 when failing to create private group. !12394 +- Fix linking to line number on side-by-side diff creating empty discussion box. +- Don't match tilde and exclamation mark as part of requirements.txt package name. +- Perform project housekeeping after importing projects. +- Fixed ctrl+enter not submit issue edit form. + +## 9.3.0 (2017-06-22) + +- Refactored gitlab:app:check into SystemCheck liberary and improve some checks. !9173 +- Add an ability to cancel attaching file and redesign attaching files UI. !9431 (blackst0ne) +- Add Aliyun OSS as the backup storage provider. !9721 (Yuanfei Zhu) +- Add suport for find_local_branches GRPC from Gitaly. !10059 +- Allow manual bypass of auto_sign_in_with_provider with a new param. !10187 (Maxime Besson) +- Redirect to user's keys index instead of user's index after a key is deleted in the admin. !10227 (Cyril Jouve) +- Changed Blame to Annotate in the UI to promote blameless culture. !10378 (Ilya Vassilevsky) +- Implement ability to update deploy keys. !10383 (Alexander Randa) +- Allow numeric values in gitlab-ci.yml. !10607 (blackst0ne) +- Add a feature test for Unicode trace. !10736 (dosuken123) +- Notes: Warning message should go away once resolved. !10823 (Jacopo Beschi @jacopo-beschi) +- Project authorizations are calculated much faster when using PostgreSQL, and nested groups support for MySQL has been removed +. !10885 +- Fix long urls in the title of commit. !10938 (Alexander Randa) +- Update gem sidekiq-cron from 0.4.4 to 0.6.0 and rufus-scheduler from 3.1.10 to 3.4.0. !10976 (dosuken123) +- Use relative paths for group/project/user avatars. !11001 (blackst0ne) +- Enable cancelling non-HEAD pending pipelines by default for all projects. !11023 +- Implement web hook logging. !11027 (Alexander Randa) +- Add indices for auto_canceled_by_id for ci_pipelines and ci_builds on PostgreSQL. !11034 +- Add post-deploy migration to clean up projects in `pending_delete` state. !11044 +- Limit User's trackable attributes, like `current_sign_in_at`, to update at most once/hour. !11053 +- Disallow multiple selections for Milestone dropdown. !11084 +- Link to commit author user page from pipelines. !11100 +- Fix the last coverage in trace log should be extracted. !11128 (dosuken123) +- Remove redirect for old issue url containing id instead of iid. !11135 (blackst0ne) +- Backported new SystemHook event: `repository_update`. !11140 +- Keep input data after creating a tag that already exists. !11155 +- Fix support for external CI services. !11176 +- Translate backend for Project & Repository pages. !11183 +- Fix LaTeX formatting for AsciiDoc wiki. !11212 +- Add foreign key for pipeline schedule owner. !11233 +- Print Go version in rake gitlab:env:info. !11241 +- Include the blob content when printing a blob page. !11247 +- Sync email address from specified omniauth provider. !11268 (Robin Bobbitt) +- Disable reference prefixes in notes for Snippets. !11278 +- Rename build_events to job_events. !11287 +- Add API support for pipeline schedule. !11307 (dosuken123) +- Use route.cache_key for project list cache key. !11325 +- Make environment table realtime. !11333 +- Cache npm modules between pipelines with yarn to speed up setup-test-env. !11343 +- Allow GitLab instance to start when InfluxDB hostname cannot be resolved. !11356 +- Add ConvDev Index page to admin area. !11377 +- Fix Git-over-HTTP error statuses and improve error messages. !11398 +- Renamed users 'Audit Log'' to 'Authentication Log'. !11400 +- Style people in issuable search bar. !11402 +- Change /builds in the URL to /-/jobs. Backward URLs were also added. !11407 +- Update password field label while editing service settings. !11431 +- Add an optional performance bar to view performance metrics for the current page. !11439 +- Update task_list to version 2.0.0. !11525 (Jared Deckard <jared.deckard@gmail.com>) +- Avoid resource intensive login checks if password is not provided. !11537 (Horatiu Eugen Vlad) +- Allow numeric pages domain. !11550 +- Exclude manual actions when checking if pipeline can be canceled. !11562 +- Add server uptime to System Info page in admin dashboard. !11590 (Justin Boltz) +- Simplify testing and saving service integrations. !11599 +- Fixed handling of the `can_push` attribute in the v3 deploy_keys api. !11607 (Richard Clamp) +- Improve user experience around slash commands in instant comments. !11612 +- Show current user immediately in issuable filters. !11630 +- Add extra context-sensitive functionality for the top right menu button. !11632 +- Reorder Issue action buttons in order of usability. !11642 +- Expose atom links with an RSS token instead of using the private token. !11647 (Alexis Reigel) +- Respect merge, instead of push, permissions for protected actions. !11648 +- Job details page update real time. !11651 +- Improve performance of ProjectFinder used in /projects API endpoint. !11666 +- Remove redundant data-turbolink attributes from links. !11672 (blackst0ne) +- Minimum postgresql version is now 9.2. !11677 +- Add protected variables which would only be passed to protected branches or protected tags. !11688 +- Introduce optimistic locking support via optional parameter last_commit_sha on File Update API. !11694 (electroma) +- Add $CI_ENVIRONMENT_URL to predefined variables for pipelines. !11695 +- Simplify project repository settings page. !11698 +- Fix pipeline_schedules pages throwing error 500. !11706 (dosuken123) +- Add performance deltas between app deployments on Merge Request widget. !11730 +- Add feature toggles and API endpoints for admins. !11747 +- Replace 'starred_projects.feature' spinach test with an rspec analog. !11752 (blackst0ne) +- Introduce an Events API. !11755 +- Display Shared Runner status in Admin Dashboard. !11783 (Ivan Chernov) +- Persist pipeline stages in the database. !11790 +- Revert the feature that would include the current user's username in the HTTP clone URL. !11792 +- Enable Gitaly by default in installations from source. !11796 +- Use zopfli compression for frontend assets. !11798 +- Add tag_list param to project api. !11799 (Ivan Chernov) +- Add changelog for improved Registry description. !11816 +- Automatically adjust project settings to match changes in project visibility. !11831 +- Add slugify project path to CI enviroment variables. !11838 (Ivan Chernov) +- Add all pipeline sources as special keywords to 'only' and 'except'. !11844 (Filip Krakowski) +- Allow pulling of container images using personal access tokens. !11845 +- Expose import_status in Projects API. !11851 (Robin Bobbitt) +- Allow admins to delete users from the admin users page. !11852 +- Allow users to be hard-deleted from the API. !11853 +- Fix hard-deleting users when they have authored issues. !11855 +- Fix missing optional path parameter in "Create project for user" API. !11868 +- Allow users to be hard-deleted from the admin panel. !11874 +- Add a Rake task to aid in rotating otp_key_base. !11881 +- Fix submodule link to then project under subgroup. !11906 +- Fix binary encoding error on MR diffs. !11929 +- Limit non-administrators to adding 100 members at a time to groups and projects. !11940 +- add bulgarian translation of cycle analytics page to I18N. !11958 (Lyubomir Vasilev) +- Make backup task to continue on corrupt repositories. !11962 +- Fix incorrect ETag cache key when relative instance URL is used. !11964 +- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm) +- Fix edit button for deploy keys available from other projects. !12301 (Alexander Randa) +- Fix passing CI_ENVIRONMENT_NAME and CI_ENVIRONMENT_SLUG for CI_ENVIRONMENT_URL. !12344 +- Disable environment list refresh due to bug https://gitlab.com/gitlab-org/gitlab-ee/issues/2677. !12347 +- Standardize timeline note margins across different viewport sizes. !12364 +- Fix Ordered Task List Items. !31483 (Jared Deckard <jared.deckard@gmail.com>) +- Upgrade dependency to Go 1.8.3. !31943 +- Add prometheus metrics on pipeline creation. +- Fix etag route not being a match for environments. +- Sort folder for environments. +- Support descriptions for snippets. +- Hide clone panel and file list when user is only a guest. (James Clark) +- Don’t create comment on JIRA if it already exists for the entity. +- Update Dashboard Groups UI with better support for subgroups. +- Confirm Project forking behaviour via the API. +- Add prometheus based metrics collection to gitlab webapp. +- Fix: Wiki is not searchable with Guest permissions. +- Center all empty states. +- Remove 'New issue' button when issues search returns no results. +- Add API URL to JIRA settings. +- animate adding issue to boards. +- Update session cookie key name to be unique to instance in development. +- Single click on filter to open filtered search dropdown. +- Makes header information of pipeline show page realtine. +- Creates a mediator for pipeline details vue in order to mount several vue apps with the same data. +- Scope issue/merge request recent searches to project. +- Increase individual diff collapse limit to 100 KB, and render limit to 200 KB. +- Fix Pipelines table empty state - only render empty state if we receive 0 pipelines. +- Make New environment empty state btn lowercase. +- Removes duplicate environment variable in documentation. +- Change links in issuable meta to black. +- Fix border-bottom for project activity tab. +- Adds new icon for CI skipped status. +- Create equal padding for emoji. +- Use briefcase icon for company in profile page. +- Remove overflow from comment form for confidential issues and vertically aligns confidential issue icon. +- Keep trailing newline when resolving conflicts by picking sides. +- Fix /unsubscribe slash command creating extra todos when you were already mentioned in an issue. +- Fix math rendering on blob pages. +- Allow group reporters to manage group labels. +- Use pre-wrap for commit messages to keep lists indented. +- Count badges depend on translucent color to better adjust to different background colors and permission badges now feature a pill shaped design similar to labels. +- Allow reporters to promote project labels to group labels. +- Enabled keyboard shortcuts on artifacts pages. +- Perform filtered search when state tab is changed. +- Remove duplication for sharing projects with groups in project settings. +- Change order of commits ahead and behind on divergence graph for branch list view. +- Creates CI Header component for Pipelines and Jobs details pages. +- Invalidate cache for issue and MR counters more granularly. +- disable blocked manual actions. +- Load tree readme asynchronously. +- Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and LICENSE blob pages. +- Fix replying to a commit discussion displayed in the context of an MR. +- Consistently use monospace font for commit SHAs and branch and tag names. +- Consistently display last push event widget. +- Don't copy empty elements that were not selected on purpose as GFM. +- Copy as GFM even when parts of other elements are selected. +- Autolink package names in Gemfile. +- Resolve N+1 query issue with discussions. +- Don't match email addresses or foo@bar as user references. +- Fix title of discussion jump button at top of page. +- Don't return nil for missing objects from parser cache. +- Make .gitmodules parsing more resilient to syntax errors. +- Add username parameter to gravatar URL. +- Autolink package names in more dependency files. +- Return nil when looking up config for unknown LDAP provider. +- Add system note with link to diff comparison when MR discussion becomes outdated. +- Don't wrap pasted code when it's already inside code tags. +- Revert 'New file from interface on existing branch'. +- Show last commit for current tree on tree page. +- Add documentation about adding foreign keys. +- add username field to push webhook. (David Turner) +- Rename CI/CD Pipelines to Pipelines in the project settings. +- Make environment tables responsive. +- Expand/collapse backlog & closed lists in issue boards. +- Fix GitHub importer performance on branch existence check. +- Fix counter cache for acts as taggable. +- Github - Fix token interpolation when cloning wiki repository. +- Fix token interpolation when setting the Github remote. +- Fix N+1 queries for non-members in comment threads. +- Fix terminals support for Kubernetes Service. +- Fix: A diff comment on a change at last line of a file shows as two comments in discussion. +- Instrument MergeRequestDiff#load_commits. +- Introduce source to Pipeline entity. +- Fixed create new label form in issue form not working for sub-group projects. +- Fixed style on unsubscribe page. (Gustav Ernberg) +- Enables inline editing for an issues title & description. +- Ask for an example project for bug reports. +- Add summary lines for collapsed details in the bug report template. +- Prevent commits from upstream repositories to be re-processed by forks. +- Avoid repeated queries for pipeline builds on merge requests. +- Preloads head pipeline for merge request collection. +- Handle head pipeline when creating merge requests. +- Migrate artifacts to a new path. +- Rescue OpenSSL::SSL::SSLError in JiraService & IssueTrackerService. +- Repository browser: handle in-repository submodule urls. (David Turner) +- Prevent project transfers if a new group is not selected. +- Allow 'no one' as an option for allowed to merge on a procted branch. +- Reduce time spent waiting for certain Sidekiq jobs to complete. +- Refactor ProjectsFinder#init_collection to produce more efficient queries for retrieving projects. +- Remove unused code and uses underscore. +- Restricts search projects dropdown to group projects when group is selected. +- Properly handle container registry redirects to fix metadata stored on a S3 backend. +- Fix LFS timeouts when trying to save large files. +- Set artifact working directory to be in the destination store to prevent unnecessary I/O. +- Strip trailing whitespaces in submodule URLs. +- Make sure reCAPTCHA configuration is loaded when spam checks are initiated. +- Fix up arrow not editing last discussion comment. +- Added application readiness endpoints to the monitoring health check admin view. +- Use wait_for_requests for both ajax and Vue requests. +- Cleanup ci_variables schema and table. +- Remove foreigh key on ci_trigger_schedules only if it exists. +- Allow translation of Pipeline Schedules. + +## 9.2.7 (2017-06-21) + +- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm) + +## 9.2.6 (2017-06-16) + +- Fix the last coverage in trace log should be extracted. !11128 (dosuken123) +- Respect merge, instead of push, permissions for protected actions. !11648 +- Fix pipeline_schedules pages throwing error 500. !11706 (dosuken123) +- Make backup task to continue on corrupt repositories. !11962 +- Fix incorrect ETag cache key when relative instance URL is used. !11964 +- Fix math rendering on blob pages. +- Invalidate cache for issue and MR counters more granularly. +- Fix terminals support for Kubernetes Service. +- Fix LFS timeouts when trying to save large files. +- Strip trailing whitespaces in submodule URLs. +- Make sure reCAPTCHA configuration is loaded when spam checks are initiated. +- Remove foreigh key on ci_trigger_schedules only if it exists. + ## 9.2.5 (2017-06-07) - No changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b6c87ae518..89e505709a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,8 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._ Thank you for your interest in contributing to GitLab. This guide details how to contribute to GitLab in a way that is efficient for everyone. +Looking for something to work on? Look for the label [Accepting Merge Requests](#i-want-to-contribute). + GitLab comes into two flavors, GitLab Community Edition (CE) our free and open source edition, and GitLab Enterprise Edition (EE) which is our commercial edition. Throughout this guide you will see references to CE and EE for diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index bc859cbd6d9..54d1a4f2a4a 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.11.2 +0.13.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index ab0fa336dd0..c20c645d7e4 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.0.5 +5.0.6 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 3e3c2f1e5ed..ccbccc3dc62 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -2.1.1 +2.2.0 @@ -2,7 +2,7 @@ source 'https://rubygems.org' gem 'rails', '4.2.8' gem 'rails-deprecated_sanitizer', '~> 1.0.3' -gem 'bootsnap', '~> 1.0.0' +gem 'bootsnap', '~> 1.1' # Responders respond_to and respond_with gem 'responders', '~> 2.0' @@ -86,7 +86,7 @@ gem 'kaminari', '~> 0.17.0' gem 'hamlit', '~> 2.6.1' # Files attachments -gem 'carrierwave', '~> 1.0' +gem 'carrierwave', '~> 1.1' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' @@ -123,6 +123,7 @@ gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor-plantuml', '0.0.7' gem 'rouge', '~> 2.0' gem 'truncato', '~> 0.7.8' +gem 'bootstrap_form', '~> 2.7.0' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM @@ -158,7 +159,7 @@ gem 'rufus-scheduler', '~> 3.4' gem 'httparty', '~> 0.13.3' # Colored output to console -gem 'rainbow', '~> 2.1.0' +gem 'rainbow', '~> 2.2' # GitLab settings gem 'settingslogic', '~> 2.0.9' @@ -256,10 +257,11 @@ gem 'base32', '~> 0.3.0' # Sentry integration gem 'sentry-raven', '~> 2.4.0' -gem 'premailer-rails', '~> 1.9.0' +gem 'premailer-rails', '~> 1.9.7' # I18n gem 'ruby_parser', '~> 3.8', require: false +gem 'rails-i18n', '~> 4.0.9' gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext', '~> 3.2.2', require: false, group: :development @@ -353,7 +355,7 @@ group :test do gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'email_spec', '~> 1.6.0' gem 'json-schema', '~> 2.6.2' - gem 'webmock', '~> 1.24.0' + gem 'webmock', '~> 2.3.2' gem 'test_after_commit', '~> 1.1' gem 'sham_rack', '~> 1.3.6' gem 'timecop', '~> 0.8.0' @@ -373,7 +375,7 @@ gem 'ruby-prof', '~> 0.16.2' gem 'oauth2', '~> 1.4' # Soft deletion -gem 'paranoia', '~> 2.2' +gem 'paranoia', '~> 2.3.1' # Health check gem 'health_check', '~> 2.6.0' @@ -383,7 +385,7 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # Gitaly GRPC client -gem 'gitaly', '~> 0.8.0' +gem 'gitaly', '~> 0.9.0' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6755c75e331..f4ddd30da1b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,11 +83,12 @@ GEM bindata (2.3.5) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootsnap (1.0.0) + bootsnap (1.1.1) msgpack (~> 1.0) bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) + bootstrap_form (2.7.0) brakeman (3.6.1) browser (2.2.0) builder (3.2.3) @@ -108,7 +109,7 @@ GEM capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy - carrierwave (1.0.0) + carrierwave (1.1.0) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) @@ -138,7 +139,7 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) creole (0.5.0) - css_parser (1.4.1) + css_parser (1.5.0) addressable d3_rails (3.5.11) railties (>= 3.1.0) @@ -277,7 +278,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly (0.8.0) + gitaly (0.9.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -353,7 +354,7 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) - grpc (1.2.5) + grpc (1.4.0) google-protobuf (~> 3.1) googleauth (~> 0.5.1) haml (4.0.7) @@ -367,7 +368,7 @@ GEM temple (~> 0.7.6) thor tilt - hashdiff (0.3.2) + hashdiff (0.3.4) hashie (3.5.5) hashie-forbidden_attributes (0.1.1) hashie (>= 3.0) @@ -462,7 +463,7 @@ GEM mimemagic (0.3.0) mini_portile2 (2.1.0) minitest (5.7.0) - mmap2 (2.2.6) + mmap2 (2.2.7) mousetrap-rails (1.4.6) msgpack (1.1.0) multi_json (1.12.1) @@ -546,8 +547,8 @@ GEM rubypants (~> 0.2) orm_adapter (0.5.0) os (0.9.6) - paranoia (2.2.0) - activerecord (>= 4.0, < 5.1) + paranoia (2.3.1) + activerecord (>= 4.0, < 5.2) parser (2.4.0.0) ast (~> 2.2) path_expander (1.0.1) @@ -591,10 +592,11 @@ GEM websocket-driver (>= 0.2.0) posix-spawn (0.3.11) powerpack (0.1.1) - premailer (1.8.6) - css_parser (>= 1.3.6) + premailer (1.10.4) + addressable + css_parser (>= 1.4.10) htmlentities (>= 4.0.0) - premailer-rails (1.9.2) + premailer-rails (1.9.7) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) prometheus-client-mmap (0.7.0.beta5) @@ -646,12 +648,16 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) + rails-i18n (4.0.9) + i18n (~> 0.7) + railties (~> 4.0) railties (4.2.8) actionpack (= 4.2.8) activesupport (= 4.2.8) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.1.0) + rainbow (2.2.2) + rake raindrops (0.17.0) rake (10.5.0) rblineprof (0.3.6) @@ -885,7 +891,7 @@ GEM vmstat (2.3.0) warden (1.2.6) rack (>= 1.0) - webmock (1.24.6) + webmock (2.3.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff @@ -924,15 +930,16 @@ DEPENDENCIES benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) binding_of_caller (~> 0.7.2) - bootsnap (~> 1.0.0) + bootsnap (~> 1.1) bootstrap-sass (~> 3.3.0) + bootstrap_form (~> 2.7.0) brakeman (~> 3.6.0) browser (~> 2.2) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) - carrierwave (~> 1.0) + carrierwave (~> 1.1) charlock_holmes (~> 0.7.3) chronic (~> 0.10.2) chronic_duration (~> 0.10.6) @@ -973,7 +980,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly (~> 0.8.0) + gitaly (~> 0.9.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) @@ -1031,7 +1038,7 @@ DEPENDENCIES omniauth-twitter (~> 1.2.0) omniauth_crowd (~> 2.2.0) org-ruby (~> 0.9.12) - paranoia (~> 2.2) + paranoia (~> 2.3.1) peek (~> 1.0.1) peek-gc (~> 0.0.2) peek-host (~> 1.0.0) @@ -1043,7 +1050,7 @@ DEPENDENCIES peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) poltergeist (~> 1.9.0) - premailer-rails (~> 1.9.0) + premailer-rails (~> 1.9.7) prometheus-client-mmap (~> 0.7.0.beta5) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) @@ -1053,7 +1060,8 @@ DEPENDENCIES rack-proxy (~> 0.6.0) rails (= 4.2.8) rails-deprecated_sanitizer (~> 1.0.3) - rainbow (~> 2.1.0) + rails-i18n (~> 4.0.9) + rainbow (~> 2.2) rblineprof (~> 0.3.6) rdoc (~> 4.2) recaptcha (~> 3.0) @@ -1114,9 +1122,9 @@ DEPENDENCIES version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.3.0) - webmock (~> 1.24.0) + webmock (~> 2.3.2) webpack-rails (~> 0.9.10) wikicloth (= 0.8.1) BUNDLED WITH - 1.15.0 + 1.15.1 diff --git a/app/assets/images/new_nav.png b/app/assets/images/new_nav.png Binary files differnew file mode 100644 index 00000000000..8879d26d341 --- /dev/null +++ b/app/assets/images/new_nav.png diff --git a/app/assets/images/old_nav.png b/app/assets/images/old_nav.png Binary files differnew file mode 100644 index 00000000000..23fae7aa19e --- /dev/null +++ b/app/assets/images/old_nav.png diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 6680834a8d1..56fa0d71a9a 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -77,7 +77,7 @@ const Api = { dataType: 'json', }) .done(label => callback(label)) - .error(message => callback(message.responseJSON)); + .fail(message => callback(message.responseJSON)); }, // Return group projects list. Filtered by query @@ -134,7 +134,7 @@ const Api = { dataType: 'json', }) .done(file => callback(null, file)) - .error(callback); + .fail(callback); }, users(query, options) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index adb45b0606d..c34d80f0601 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,11 +1,8 @@ +/* eslint-disable class-methods-use-this */ /* global Flash */ import Cookies from 'js-cookie'; - -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; -import { glEmojiTag } from './behaviors/gl_emoji'; -import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid'; +import * as Emoji from './emoji'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -16,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame || const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence -let categoryMap = null; - const categoryLabelMap = { activity: 'Activity', people: 'People', @@ -29,26 +24,6 @@ const categoryLabelMap = { flags: 'Flags', }; -function buildCategoryMap() { - return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => { - const emojiInfo = emojiMap[emojiNameKey]; - if (currentCategoryMap[emojiInfo.category]) { - currentCategoryMap[emojiInfo.category].push(emojiNameKey); - } - - return currentCategoryMap; - }, { - activity: [], - people: [], - nature: [], - food: [], - travel: [], - objects: [], - symbols: [], - flags: [], - }); -} - function renderCategory(name, emojiList, opts = {}) { return ` <h5 class="emoji-menu-title"> @@ -58,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) { ${emojiList.map(emojiName => ` <li class="emoji-menu-list-item"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> - ${glEmojiTag(emojiName, { + ${Emoji.glEmojiTag(emojiName, { sprite: true, })} </button> @@ -68,147 +43,143 @@ function renderCategory(name, emojiList, opts = {}) { `; } -function AwardsHandler() { - this.eventListeners = []; - this.aliases = emojiAliases; - // If the user shows intent let's pre-build the menu - this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { - const $menu = $('.emoji-menu'); - if ($menu.length === 0) { - requestAnimationFrame(() => { - this.createEmojiMenu(); - }); - } - // Prebuild the categoryMap - categoryMap = categoryMap || buildCategoryMap(); - }); - this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.showEmojiMenu($(e.currentTarget)); - }); - - this.registerEventListener('on', $('html'), 'click', (e) => { - const $target = $(e.target); - if (!$target.closest('.emoji-menu-content').length) { - $('.js-awards-block.current').removeClass('current'); - } - if (!$target.closest('.emoji-menu').length) { - if ($('.emoji-menu').is(':visible')) { - $('.js-add-award.is-active').removeClass('is-active'); - $('.emoji-menu').removeClass('is-visible'); +export default class AwardsHandler { + constructor() { + this.eventListeners = []; + // If the user shows intent let's pre-build the menu + this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { + const $menu = $('.emoji-menu'); + if ($menu.length === 0) { + requestAnimationFrame(() => { + this.createEmojiMenu(); + }); } - } - }); - this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => { - e.preventDefault(); - const $target = $(e.currentTarget); - const $glEmojiElement = $target.find('gl-emoji'); - const $spriteIconElement = $target.find('.icon'); - const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); - - $target.closest('.js-awards-block').addClass('current'); - this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); - }); -} + }); + this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.showEmojiMenu($(e.currentTarget)); + }); -AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) { - element[method].call(element, ...args); - this.eventListeners.push({ - element, - args, - }); -}; + this.registerEventListener('on', $('html'), 'click', (e) => { + const $target = $(e.target); + if (!$target.closest('.emoji-menu-content').length) { + $('.js-awards-block.current').removeClass('current'); + } + if (!$target.closest('.emoji-menu').length) { + if ($('.emoji-menu').is(':visible')) { + $('.js-add-award.is-active').removeClass('is-active'); + $('.emoji-menu').removeClass('is-visible'); + } + } + }); + this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + const $glEmojiElement = $target.find('gl-emoji'); + const $spriteIconElement = $target.find('.icon'); + const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); + + $target.closest('.js-awards-block').addClass('current'); + this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); + }); + } -AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { - if ($addBtn.hasClass('js-note-emoji')) { - $addBtn.closest('.note').find('.js-awards-block').addClass('current'); - } else { - $addBtn.closest('.js-awards-block').addClass('current'); - } - - const $menu = $('.emoji-menu'); - const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent(); - const $userAuthored = this.isUserAuthored($addBtn); - if ($menu.length) { - if ($menu.is('.is-visible')) { - $addBtn.removeClass('is-active'); - $menu.removeClass('is-visible'); - $('.js-emoji-menu-search').blur(); - } else { - $addBtn.addClass('is-active'); - this.positionMenu($menu, $addBtn); - $menu.addClass('is-visible'); - $('.js-emoji-menu-search').focus(); - } - } else { - $addBtn.addClass('is-loading is-active'); - this.createEmojiMenu(() => { - const $createdMenu = $('.emoji-menu'); - $addBtn.removeClass('is-loading'); - this.positionMenu($createdMenu, $addBtn); - return setTimeout(() => { - $createdMenu.addClass('is-visible'); - $('.js-emoji-menu-search').focus(); - }, 200); + registerEventListener(method = 'on', element, ...args) { + element[method].call(element, ...args); + this.eventListeners.push({ + element, + args, }); } - $thumbsBtn.toggleClass('disabled', $userAuthored); -}; + showEmojiMenu($addBtn) { + if ($addBtn.hasClass('js-note-emoji')) { + $addBtn.closest('.note').find('.js-awards-block').addClass('current'); + } else { + $addBtn.closest('.js-awards-block').addClass('current'); + } -// Create the emoji menu with the first category of emojis. -// Then render the remaining categories of emojis one by one to avoid jank. -AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { - if (this.isCreatingEmojiMenu) { - return; - } - this.isCreatingEmojiMenu = true; - - // Render the first category - categoryMap = categoryMap || buildCategoryMap(); - const categoryNameKey = Object.keys(categoryMap)[0]; - const emojisInCategory = categoryMap[categoryNameKey]; - const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); - - // Render the frequently used - const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); - let frequentlyUsedCatgegory = ''; - if (frequentlyUsedEmojis.length > 0) { - frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { - menuListClass: 'frequent-emojis', - }); + const $menu = $('.emoji-menu'); + const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent(); + const $userAuthored = this.isUserAuthored($addBtn); + if ($menu.length) { + if ($menu.is('.is-visible')) { + $addBtn.removeClass('is-active'); + $menu.removeClass('is-visible'); + $('.js-emoji-menu-search').blur(); + } else { + $addBtn.addClass('is-active'); + this.positionMenu($menu, $addBtn); + $menu.addClass('is-visible'); + $('.js-emoji-menu-search').focus(); + } + } else { + $addBtn.addClass('is-loading is-active'); + this.createEmojiMenu(() => { + const $createdMenu = $('.emoji-menu'); + $addBtn.removeClass('is-loading'); + this.positionMenu($createdMenu, $addBtn); + return setTimeout(() => { + $createdMenu.addClass('is-visible'); + $('.js-emoji-menu-search').focus(); + }, 200); + }); + } + + $thumbsBtn.toggleClass('disabled', $userAuthored); } - const emojiMenuMarkup = ` - <div class="emoji-menu"> - <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" /> + // Create the emoji menu with the first category of emojis. + // Then render the remaining categories of emojis one by one to avoid jank. + createEmojiMenu(callback) { + if (this.isCreatingEmojiMenu) { + return; + } + this.isCreatingEmojiMenu = true; + + // Render the first category + const categoryMap = Emoji.getEmojiCategoryMap(); + const categoryNameKey = Object.keys(categoryMap)[0]; + const emojisInCategory = categoryMap[categoryNameKey]; + const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + + // Render the frequently used + const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + let frequentlyUsedCatgegory = ''; + if (frequentlyUsedEmojis.length > 0) { + frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { + menuListClass: 'frequent-emojis', + }); + } + + const emojiMenuMarkup = ` + <div class="emoji-menu"> + <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" /> - <div class="emoji-menu-content"> - ${frequentlyUsedCatgegory} - ${firstCategory} + <div class="emoji-menu-content"> + ${frequentlyUsedCatgegory} + ${firstCategory} + </div> </div> - </div> - `; + `; - document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); + document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); - this.addRemainingEmojiMenuCategories(); - this.setupSearch(); - if (callback) { - callback(); + this.addRemainingEmojiMenuCategories(); + this.setupSearch(); + if (callback) { + callback(); + } } -}; -AwardsHandler - .prototype - .addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() { + addRemainingEmojiMenuCategories() { if (this.isAddingRemainingEmojiMenuCategories) { return; } this.isAddingRemainingEmojiMenuCategories = true; - categoryMap = categoryMap || buildCategoryMap(); + const categoryMap = Emoji.getEmojiCategoryMap(); // Avoid the jank and render the remaining categories separately // This will take more time, but makes UI more responsive @@ -243,179 +214,167 @@ AwardsHandler emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>'); throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`); }); - }; - -AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) { - const position = $addBtn.data('position'); - // The menu could potentially be off-screen or in a hidden overflow element - // So we position the element absolute in the body - const css = { - top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, - }; - if (position === 'right') { - css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`; - $menu.addClass('is-aligned-right'); - } else { - css.left = `${$addBtn.offset().left}px`; - $menu.removeClass('is-aligned-right'); - } - return $menu.css(css); -}; - -AwardsHandler.prototype.addAward = function addAward( - votesBlock, - awardUrl, - emoji, - checkMutuality, - callback, -) { - const normalizedEmoji = this.normalizeEmojiName(emoji); - const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); - this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { - this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); - return typeof callback === 'function' ? callback() : undefined; - }); - $('.emoji-menu').removeClass('is-visible'); - $('.js-add-award.is-active').removeClass('is-active'); -}; + } -AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( - votesBlock, - emoji, - checkForMutuality, -) { - if (checkForMutuality || checkForMutuality === null) { - this.checkMutuality(votesBlock, emoji); - } - this.addEmojiToFrequentlyUsedList(emoji); - const normalizedEmoji = this.normalizeEmojiName(emoji); - const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); - if ($emojiButton.length > 0) { - if (this.isActive($emojiButton)) { - this.decrementCounter($emojiButton, normalizedEmoji); + positionMenu($menu, $addBtn) { + const position = $addBtn.data('position'); + // The menu could potentially be off-screen or in a hidden overflow element + // So we position the element absolute in the body + const css = { + top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, + }; + if (position === 'right') { + css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`; + $menu.addClass('is-aligned-right'); } else { - const counter = $emojiButton.find('.js-counter'); - counter.text(parseInt(counter.text(), 10) + 1); - $emojiButton.addClass('active'); - this.addYouToUserList(votesBlock, normalizedEmoji); - this.animateEmoji($emojiButton); + css.left = `${$addBtn.offset().left}px`; + $menu.removeClass('is-aligned-right'); } - } else { - votesBlock.removeClass('hidden'); - this.createEmoji(votesBlock, normalizedEmoji); + return $menu.css(css); } -}; -AwardsHandler.prototype.getVotesBlock = function getVotesBlock() { - const currentBlock = $('.js-awards-block.current'); - let resultantVotesBlock = currentBlock; - if (currentBlock.length === 0) { - resultantVotesBlock = $('.js-awards-block').eq(0); + addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { + const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { + this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); + return typeof callback === 'function' ? callback() : undefined; + }); + $('.emoji-menu').removeClass('is-visible'); + $('.js-add-award.is-active').removeClass('is-active'); } - return resultantVotesBlock; -}; + addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) { + if (checkForMutuality || checkForMutuality === null) { + this.checkMutuality(votesBlock, emoji); + } + this.addEmojiToFrequentlyUsedList(emoji); + const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + if ($emojiButton.length > 0) { + if (this.isActive($emojiButton)) { + this.decrementCounter($emojiButton, normalizedEmoji); + } else { + const counter = $emojiButton.find('.js-counter'); + counter.text(parseInt(counter.text(), 10) + 1); + $emojiButton.addClass('active'); + this.addYouToUserList(votesBlock, normalizedEmoji); + this.animateEmoji($emojiButton); + } + } else { + votesBlock.removeClass('hidden'); + this.createEmoji(votesBlock, normalizedEmoji); + } + } -AwardsHandler.prototype.getAwardUrl = function getAwardUrl() { - return this.getVotesBlock().data('award-url'); -}; + getVotesBlock() { + const currentBlock = $('.js-awards-block.current'); + let resultantVotesBlock = currentBlock; + if (currentBlock.length === 0) { + resultantVotesBlock = $('.js-awards-block').eq(0); + } + + return resultantVotesBlock; + } + + getAwardUrl() { + return this.getVotesBlock().data('award-url'); + } -AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) { - const awardUrl = this.getAwardUrl(); - if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; - const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent(); - const isAlreadyVoted = $emojiButton.hasClass('active'); - if (isAlreadyVoted) { - this.addAward(votesBlock, awardUrl, mutualVote, false); + checkMutuality(votesBlock, emoji) { + const awardUrl = this.getAwardUrl(); + if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent(); + const isAlreadyVoted = $emojiButton.hasClass('active'); + if (isAlreadyVoted) { + this.addAward(votesBlock, awardUrl, mutualVote, false); + } } } -}; -AwardsHandler.prototype.isActive = function isActive($emojiButton) { - return $emojiButton.hasClass('active'); -}; + isActive($emojiButton) { + return $emojiButton.hasClass('active'); + } -AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) { - return $button.hasClass('js-user-authored'); -}; + isUserAuthored($button) { + return $button.hasClass('js-user-authored'); + } -AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) { - const counter = $('.js-counter', $emojiButton); - const counterNumber = parseInt(counter.text(), 10); - if (counterNumber > 1) { - counter.text(counterNumber - 1); - this.removeYouFromUserList($emojiButton); - } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - $emojiButton.tooltip('destroy'); - counter.text('0'); - this.removeYouFromUserList($emojiButton); - if ($emojiButton.parents('.note').length) { + decrementCounter($emojiButton, emoji) { + const counter = $('.js-counter', $emojiButton); + const counterNumber = parseInt(counter.text(), 10); + if (counterNumber > 1) { + counter.text(counterNumber - 1); + this.removeYouFromUserList($emojiButton); + } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + $emojiButton.tooltip('destroy'); + counter.text('0'); + this.removeYouFromUserList($emojiButton); + if ($emojiButton.parents('.note').length) { + this.removeEmoji($emojiButton); + } + } else { this.removeEmoji($emojiButton); } - } else { - this.removeEmoji($emojiButton); + return $emojiButton.removeClass('active'); } - return $emojiButton.removeClass('active'); -}; -AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) { - $emojiButton.tooltip('destroy'); - $emojiButton.remove(); - const $votesBlock = this.getVotesBlock(); - if ($votesBlock.find('.js-emoji-btn').length === 0) { - $votesBlock.addClass('hidden'); + removeEmoji($emojiButton) { + $emojiButton.tooltip('destroy'); + $emojiButton.remove(); + const $votesBlock = this.getVotesBlock(); + if ($votesBlock.find('.js-emoji-btn').length === 0) { + $votesBlock.addClass('hidden'); + } } -}; - -AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) { - return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; -}; -AwardsHandler.prototype.toSentence = function toSentence(list) { - let sentence; - if (list.length <= 2) { - sentence = list.join(' and '); - } else { - sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`; + getAwardTooltip($awardBlock) { + return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; } - return sentence; -}; + toSentence(list) { + let sentence; + if (list.length <= 2) { + sentence = list.join(' and '); + } else { + sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`; + } -AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) { - const awardBlock = $emojiButton; - const originalTitle = this.getAwardTooltip(awardBlock); - const authors = originalTitle.split(FROM_SENTENCE_REGEX); - authors.splice(authors.indexOf('You'), 1); - return awardBlock - .closest('.js-emoji-btn') - .removeData('title') - .removeAttr('data-title') - .removeAttr('data-original-title') - .attr('title', this.toSentence(authors)) - .tooltip('fixTitle'); -}; + return sentence; + } -AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) { - const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); - const origTitle = this.getAwardTooltip(awardBlock); - let users = []; - if (origTitle) { - users = origTitle.trim().split(FROM_SENTENCE_REGEX); - } - users.unshift('You'); - return awardBlock - .attr('title', this.toSentence(users)) - .tooltip('fixTitle'); -}; + removeYouFromUserList($emojiButton) { + const awardBlock = $emojiButton; + const originalTitle = this.getAwardTooltip(awardBlock); + const authors = originalTitle.split(FROM_SENTENCE_REGEX); + authors.splice(authors.indexOf('You'), 1); + return awardBlock + .closest('.js-emoji-btn') + .removeData('title') + .removeAttr('data-title') + .removeAttr('data-original-title') + .attr('title', this.toSentence(authors)) + .tooltip('fixTitle'); + } + + addYouToUserList(votesBlock, emoji) { + const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); + const origTitle = this.getAwardTooltip(awardBlock); + let users = []; + if (origTitle) { + users = origTitle.trim().split(FROM_SENTENCE_REGEX); + } + users.unshift('You'); + return awardBlock + .attr('title', this.toSentence(users)) + .tooltip('fixTitle'); + } -AwardsHandler - .prototype - .createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) { + createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> - ${glEmojiTag(emojiName)} + ${Emoji.glEmojiTag(emojiName)} <span class="award-control-text js-counter">1</span> </button> `; @@ -424,144 +383,127 @@ AwardsHandler this.animateEmoji($emojiButton); $('.award-control').tooltip(); votesBlock.removeClass('current'); - }; - -AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) { - const className = 'pulse animated once short'; - $emoji.addClass(className); + } - this.registerEventListener('on', $emoji, animationEndEventString, (e) => { - $(e.currentTarget).removeClass(className); - }); -}; + animateEmoji($emoji) { + const className = 'pulse animated once short'; + $emoji.addClass(className); -AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) { - if ($('.emoji-menu').length) { - this.createAwardButtonForVotesBlock(votesBlock, emoji); + this.registerEventListener('on', $emoji, animationEndEventString, (e) => { + $(e.currentTarget).removeClass(className); + }); } - this.createEmojiMenu(() => { - this.createAwardButtonForVotesBlock(votesBlock, emoji); - }); -}; -AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) { - if (this.isUserAuthored($emojiButton)) { - this.userAuthored($emojiButton); - } else { - $.post(awardUrl, { - name: emoji, - }, (data) => { - if (data.ok) { - callback(); - } - }).fail(() => new Flash('Something went wrong on our end.')); + createEmoji(votesBlock, emoji) { + if ($('.emoji-menu').length) { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + } + this.createEmojiMenu(() => { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + }); } -}; -AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) { - return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); -}; + postEmoji($emojiButton, awardUrl, emoji, callback) { + if (this.isUserAuthored($emojiButton)) { + this.userAuthored($emojiButton); + } else { + $.post(awardUrl, { + name: emoji, + }, (data) => { + if (data.ok) { + callback(); + } + }).fail(() => new Flash('Something went wrong on our end.')); + } + } -AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) { - const oldTitle = this.getAwardTooltip($emojiButton); - const newTitle = 'You cannot vote on your own issue, MR and note'; - gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show'); - // Restore tooltip back to award list - return setTimeout(() => { - $emojiButton.tooltip('hide'); - gl.utils.updateTooltipTitle($emojiButton, oldTitle); - }, 2800); -}; + findEmojiIcon(votesBlock, emoji) { + return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); + } -AwardsHandler.prototype.scrollToAwards = function scrollToAwards() { - const options = { - scrollTop: $('.awards').offset().top - 110, - }; - return $('body, html').animate(options, 200); -}; + userAuthored($emojiButton) { + const oldTitle = this.getAwardTooltip($emojiButton); + const newTitle = 'You cannot vote on your own issue, MR and note'; + gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show'); + // Restore tooltip back to award list + return setTimeout(() => { + $emojiButton.tooltip('hide'); + gl.utils.updateTooltipTitle($emojiButton, oldTitle); + }, 2800); + } -AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) { - return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji; -}; + scrollToAwards() { + const options = { + scrollTop: $('.awards').offset().top - 110, + }; + return $('body, html').animate(options, 200); + } -AwardsHandler - .prototype - .addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) { - if (isEmojiNameValid(emoji)) { + addEmojiToFrequentlyUsedList(emoji) { + if (Emoji.isEmojiNameValid(emoji)) { this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); } - }; - -AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() { - return this.frequentlyUsedEmojis || (() => { - const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); - this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( - inputName => isEmojiNameValid(inputName), - ); + } - return this.frequentlyUsedEmojis; - })(); -}; + getFrequentlyUsedEmojis() { + return this.frequentlyUsedEmojis || (() => { + const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); + this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( + inputName => Emoji.isEmojiNameValid(inputName), + ); -AwardsHandler.prototype.setupSearch = function setupSearch() { - const $search = $('.js-emoji-menu-search'); + return this.frequentlyUsedEmojis; + })(); + } - this.registerEventListener('on', $search, 'input', (e) => { - const term = $(e.target).val().trim(); - this.searchEmojis(term); - }); + setupSearch() { + const $search = $('.js-emoji-menu-search'); - const $menu = $('.emoji-menu'); - this.registerEventListener('on', $menu, transitionEndEventString, (e) => { - if (e.target === e.currentTarget) { - // Clear the search - this.searchEmojis(''); - } - }); -}; + this.registerEventListener('on', $search, 'input', (e) => { + const term = $(e.target).val().trim(); + this.searchEmojis(term); + }); -AwardsHandler.prototype.searchEmojis = function searchEmojis(term) { - const $search = $('.js-emoji-menu-search'); - $search.val(term); - - // Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search-title').remove(); - if (term.length > 0) { - // Generate a search result block - const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); - const foundEmojis = this.findMatchingEmojiElements(term).show(); - const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); - $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); - $('.emoji-menu-content').append(h5).append(ul); - } else { - $('.emoji-menu-content').children().show(); + const $menu = $('.emoji-menu'); + this.registerEventListener('on', $menu, transitionEndEventString, (e) => { + if (e.target === e.currentTarget) { + // Clear the search + this.searchEmojis(''); + } + }); } -}; -AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) { - const safeTerm = term.toLowerCase(); - - const namesMatchingAlias = []; - Object.keys(emojiAliases).forEach((alias) => { - if (alias.indexOf(safeTerm) >= 0) { - namesMatchingAlias.push(emojiAliases[alias]); + searchEmojis(term) { + const $search = $('.js-emoji-menu-search'); + $search.val(term); + + // Clean previous search results + $('ul.emoji-menu-search, h5.emoji-search-title').remove(); + if (term.length > 0) { + // Generate a search result block + const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); + const foundEmojis = this.findMatchingEmojiElements(term).show(); + const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); + $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); + $('.emoji-menu-content').append(h5).append(ul); + } else { + $('.emoji-menu-content').children().show(); } - }); - const $matchingElements = namesMatchingAlias.concat(safeTerm) - .reduce( - ($result, searchTerm) => - $result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)), - $([]), - ); - return $matchingElements.closest('li').clone(); -}; + } -AwardsHandler.prototype.destroy = function destroy() { - this.eventListeners.forEach((entry) => { - entry.element.off.call(entry.element, ...entry.args); - }); - $('.emoji-menu').remove(); -}; + findMatchingEmojiElements(query) { + const emojiMatches = Emoji.filterEmojiNamesByAlias(query); + const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); + const $matchingElements = $emojiElements + .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); + return $matchingElements.closest('li').clone(); + } -export default AwardsHandler; + destroy() { + this.eventListeners.forEach((entry) => { + entry.element.off.call(entry.element, ...entry.args); + }); + $('.emoji-menu').remove(); + } +} diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 36ce4fddb72..8156e491a42 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,75 +1,10 @@ import installCustomElements from 'document-register-element'; -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; -import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map'; -import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported'; +import { emojiImageTag, emojiFallbackImageSrc } from '../emoji'; +import isEmojiUnicodeSupported from '../emoji/support'; installCustomElements(window); -const generatedUnicodeSupportMap = getUnicodeSupportMap(); - -function emojiImageTag(name, src) { - return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; -} - -function assembleFallbackImageSrc(inputName) { - let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? - emojiAliases[inputName] : inputName; - let emojiInfo = emojiMap[name]; - // Fallback to question mark for unknown emojis - if (!emojiInfo) { - name = 'grey_question'; - emojiInfo = emojiMap[name]; - } - const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`; - - return fallbackImageSrc; -} -const glEmojiTagDefaults = { - sprite: false, - forceFallback: false, -}; -function glEmojiTag(inputName, options) { - const opts = Object.assign({}, glEmojiTagDefaults, options); - let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? - emojiAliases[inputName] : inputName; - let emojiInfo = emojiMap[name]; - // Fallback to question mark for unknown emojis - if (!emojiInfo) { - name = 'grey_question'; - emojiInfo = emojiMap[name]; - } - - const fallbackImageSrc = assembleFallbackImageSrc(name); - const fallbackSpriteClass = `emoji-${name}`; - - const classList = []; - if (opts.forceFallback && opts.sprite) { - classList.push('emoji-icon'); - classList.push(fallbackSpriteClass); - } - const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; - const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; - let contents = emojiInfo.moji; - if (opts.forceFallback && !opts.sprite) { - contents = emojiImageTag(name, fallbackImageSrc); - } - - return ` - <gl-emoji - ${classAttribute} - data-name="${name}" - data-fallback-src="${fallbackImageSrc}" - ${fallbackSpriteAttribute} - data-unicode-version="${emojiInfo.unicodeVersion}" - title="${emojiInfo.description}" - > - ${contents} - </gl-emoji> - `; -} - -function installGlEmojiElement() { +export default function installGlEmojiElement() { const GlEmojiElementProto = Object.create(HTMLElement.prototype); GlEmojiElementProto.createdCallback = function createdCallback() { const emojiUnicode = this.textContent.trim(); @@ -90,7 +25,7 @@ function installGlEmojiElement() { if ( emojiUnicode && isEmojiUnicode && - !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) + !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion) ) { // CSS sprite fallback takes precedence over image fallback if (hasCssSpriteFalback) { @@ -100,7 +35,7 @@ function installGlEmojiElement() { } else if (hasImageFallback) { this.innerHTML = emojiImageTag(name, fallbackSrc); } else { - const src = assembleFallbackImageSrc(name); + const src = emojiFallbackImageSrc(name); this.innerHTML = emojiImageTag(name, src); } } @@ -110,9 +45,3 @@ function installGlEmojiElement() { prototype: GlEmojiElementProto, }); } - -export { - installGlEmojiElement, - glEmojiTag, - emojiImageTag, -}; diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js deleted file mode 100644 index be4aeb32c46..00000000000 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js +++ /dev/null @@ -1,11 +0,0 @@ -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; - -function isEmojiNameValid(inputName) { - const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? - emojiAliases[inputName] : inputName; - - return name && emojiMap[name]; -} - -export default isEmojiNameValid; diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 5b931e6cfa6..44b2c974b9e 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,7 +1,7 @@ import './autosize'; import './bind_in_out'; import './details_behavior'; -import { installGlEmojiElement } from './gl_emoji'; +import installGlEmojiElement from './gl_emoji'; import './quick_submit'; import './requires_input'; import './toggler_behavior'; diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 1f9e0448084..bc693616460 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { e.preventDefault(); const $form = $(e.target).closest('form'); - const $submitButton = $form.find('input[type=submit], button[type=submit]'); + const $submitButton = $form.find('input[type=submit], button[type=submit]').first(); if (!$submitButton.attr('disabled')) { $submitButton.trigger('click', [e]); diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js index b1c47b09c35..4af8b0c7713 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -17,7 +17,7 @@ export default { methods: { submit(e) { e.preventDefault(); - if (this.title.trim() === '') return; + if (this.title.trim() === '') return Promise.resolve(); this.error = false; @@ -29,7 +29,10 @@ export default { assignees: [], }); - this.list.newIssue(issue) + eventHub.$emit(`scroll-board-list-${this.list.id}`); + this.cancel(); + + return this.list.newIssue(issue) .then(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); @@ -47,9 +50,6 @@ export default { // Show error message this.error = true; }); - - eventHub.$emit(`scroll-board-list-${this.list.id}`); - this.cancel(); }, cancel() { this.title = ''; diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index b37698fe9ca..3f083655f95 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -11,7 +11,6 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; - this.cantEdit = cantEdit; } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 548de1a4c52..b4b09b3876e 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -112,8 +112,7 @@ class List { .then((resp) => { const data = resp.json(); issue.id = data.iid; - }) - .then(() => { + if (this.issuesSize > 1) { const moveBeforeIid = this.issues[1].id; gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 86d99dd87da..2c38440a2af 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -1,29 +1,30 @@ -/* eslint-disable no-param-reassign */ - import Vue from 'vue'; -import VueResource from 'vue-resource'; -import CommitPipelinesTable from './pipelines_table'; - -Vue.use(VueResource); +import commitPipelinesTable from './pipelines_table.vue'; /** - * Commits View > Pipelines Tab > Pipelines Table. - * - * Renders Pipelines table in pipelines tab in the commits show view. + * Used in: + * - Commit details View > Pipelines Tab > Pipelines Table. + * - Merge Request details View > Pipelines Tab > Pipelines Table. + * - New Merge Request View > Pipelines Tab > Pipelines Table. */ -// export for use in merge_request_tabs.js (TODO: remove this hack) +const CommitPipelinesTable = Vue.extend(commitPipelinesTable); + +// export for use in merge_request_tabs.js (TODO: remove this hack when we understand how to load +// vue.js in merge_request_tabs.js) window.gl = window.gl || {}; window.gl.CommitPipelinesTable = CommitPipelinesTable; -$(() => { - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - +document.addEventListener('DOMContentLoaded', () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { - gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); - pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); + const table = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + pipelineTableViewEl.appendChild(table.$el); } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js deleted file mode 100644 index 082fbafb740..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ /dev/null @@ -1,191 +0,0 @@ -import Vue from 'vue'; -import Visibility from 'visibilityjs'; -import pipelinesTableComponent from '../../vue_shared/components/pipelines_table'; -import PipelinesService from '../../pipelines/services/pipelines_service'; -import PipelineStore from '../../pipelines/stores/pipelines_store'; -import eventHub from '../../pipelines/event_hub'; -import emptyState from '../../pipelines/components/empty_state.vue'; -import errorState from '../../pipelines/components/error_state.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import '../../lib/utils/common_utils'; -import '../../vue_shared/vue_resource_interceptor'; -import Poll from '../../lib/utils/poll'; - -/** - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as `endpoint`. - * We need a store to store the received environemnts. - * We need a service to communicate with the server. - * - */ - -export default Vue.component('pipelines-table', { - - components: { - pipelinesTableComponent, - errorState, - emptyState, - loadingIcon, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} - */ - data() { - const store = new PipelineStore(); - - return { - endpoint: null, - helpPagePath: null, - store, - state: store.state, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, - }; - }, - - computed: { - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, - - /** - * Empty state is only rendered if after the first request we receive no pipelines. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.state.pipelines.length && - !this.isLoading && - this.hasMadeRequest && - !this.hasError; - }, - - shouldRenderTable() { - return !this.isLoading && - this.state.pipelines.length > 0 && - !this.hasError; - }, - }, - - /** - * When the component is about to be mounted, tell the service to fetch the data - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - beforeMount() { - const element = document.querySelector('#commit-pipeline-table-view'); - - this.endpoint = element.dataset.endpoint; - this.helpPagePath = element.dataset.helpPagePath; - this.service = new PipelinesService(this.endpoint); - - this.poll = new Poll({ - resource: this.service, - method: 'getPipelines', - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - this.poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - - beforeDestroy() { - eventHub.$off('refreshPipelines'); - }, - - destroyed() { - this.poll.stop(); - }, - - methods: { - fetchPipelines() { - this.isLoading = true; - - return this.service.getPipelines() - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - }, - - successCallback(resp) { - const response = resp.json(); - - this.hasMadeRequest = true; - - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = response.pipelines || response; - this.store.storePipelines(pipelines); - this.isLoading = false; - this.updateGraphDropdown = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } - }, - }, - - template: ` - <div class="content-list pipelines"> - - <loading-icon - label="Loading pipelines" - size="3" - v-if="isLoading" - /> - - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" /> - - <error-state v-if="shouldRenderErrorState" /> - - <div - class="table-holder" - v-if="shouldRenderTable"> - <pipelines-table-component - :pipelines="state.pipelines" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue new file mode 100644 index 00000000000..3c77f14d533 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -0,0 +1,90 @@ +<script> + import PipelinesService from '../../pipelines/services/pipelines_service'; + import PipelineStore from '../../pipelines/stores/pipelines_store'; + import pipelinesMixin from '../../pipelines/mixins/pipelines'; + + export default { + props: { + endpoint: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + mixins: [ + pipelinesMixin, + ], + + data() { + const store = new PipelineStore(); + + return { + store, + state: store.state, + }; + }, + + computed: { + /** + * Empty state is only rendered if after the first request we receive no pipelines. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.state.pipelines.length && + !this.isLoading && + this.hasMadeRequest && + !this.hasError; + }, + + shouldRenderTable() { + return !this.isLoading && + this.state.pipelines.length > 0 && + !this.hasError; + }, + }, + created() { + this.service = new PipelinesService(this.endpoint); + }, + methods: { + successCallback(resp) { + const response = resp.json(); + + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = response.pipelines || response; + this.setCommonData(pipelines); + }, + }, + }; +</script> +<template> + <div class="content-list pipelines"> + + <loading-icon + label="Loading pipelines" + size="3" + v-if="isLoading" + /> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" + /> + + <error-state + v-if="shouldRenderErrorState" + /> + + <div + class="table-holder" + v-if="shouldRenderTable"> + <pipelines-table-component + :pipelines="state.pipelines" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 5f87a05067b..31a86090242 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -55,6 +55,7 @@ import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; import ShortcutsBlob from './shortcuts_blob'; import initSettingsPanels from './settings_panels'; +import initExperimentalFlags from './experimental_flags'; (function() { var Dispatcher; @@ -79,7 +80,18 @@ import initSettingsPanels from './settings_panels'; path = page.split(':'); shortcut_handler = null; - new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); + $('.js-gfm-input').each((i, el) => { + const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete); + gfm.setup($(el), { + emojis: true, + members: enableGFM, + issues: enableGFM, + milestones: enableGFM, + mergeRequests: enableGFM, + labels: enableGFM, + }); + }); function initBlob() { new LineHighlighter(); @@ -109,6 +121,9 @@ import initSettingsPanels from './settings_panels'; } switch (page) { + case 'profiles:preferences:show': + initExperimentalFlags(); + break; case 'sessions:new': new UsernameValidator(); new ActiveTabMemoizer(); @@ -176,7 +191,7 @@ import initSettingsPanels from './settings_panels'; case 'groups:milestones:update': new ZenMode(); new gl.DueDateSelectors(); - new gl.GLForm($('.milestone-form')); + new gl.GLForm($('.milestone-form'), true); break; case 'projects:compare:show': new gl.Diff(); @@ -188,7 +203,7 @@ import initSettingsPanels from './settings_panels'; case 'projects:issues:new': case 'projects:issues:edit': shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.issue-form')); + new gl.GLForm($('.issue-form'), true); new IssuableForm($('.issue-form')); new LabelsSelect(); new MilestoneSelect(); @@ -199,7 +214,7 @@ import initSettingsPanels from './settings_panels'; case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.merge-request-form')); + new gl.GLForm($('.merge-request-form'), true); new IssuableForm($('.merge-request-form')); new LabelsSelect(); new MilestoneSelect(); @@ -208,22 +223,24 @@ import initSettingsPanels from './settings_panels'; break; case 'projects:tags:new': new ZenMode(); - new gl.GLForm($('.tag-form')); + new gl.GLForm($('.tag-form'), true); new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); break; case 'projects:snippets:new': case 'projects:snippets:edit': case 'projects:snippets:create': case 'projects:snippets:update': + new gl.GLForm($('.snippet-form'), true); + break; case 'snippets:new': case 'snippets:edit': case 'snippets:create': case 'snippets:update': - new gl.GLForm($('.snippet-form')); + new gl.GLForm($('.snippet-form'), false); break; case 'projects:releases:edit': new ZenMode(); - new gl.GLForm($('.release-form')); + new gl.GLForm($('.release-form'), true); break; case 'projects:merge_requests:show': new gl.Diff(); @@ -471,7 +488,7 @@ import initSettingsPanels from './settings_panels'; new gl.Wikis(); shortcut_handler = new ShortcutsWiki(); new ZenMode(); - new gl.GLForm($('.wiki-form')); + new gl.GLForm($('.wiki-form'), true); break; case 'snippets': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 98ddcc20036..73675d300be 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -287,6 +287,10 @@ window.DropzoneInput = (function() { $uploadingErrorMessage.html(message); }; + closeAlertMessage = function() { + return form.find('.div-dropzone-alert').alert('close'); + }; + form.find('.markdown-selector').click(function(e) { e.preventDefault(); $(this).closest('.gfm-form').find('.div-dropzone').click(); diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js new file mode 100644 index 00000000000..cac35d6eed5 --- /dev/null +++ b/app/assets/javascripts/emoji/index.js @@ -0,0 +1,99 @@ +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; + +export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; + +export function normalizeEmojiName(name) { + return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name; +} + +export function isEmojiNameValid(name) { + return validEmojiNames.indexOf(name) >= 0; +} + +export function filterEmojiNames(filter) { + const match = filter.toLowerCase(); + return validEmojiNames.filter(name => name.indexOf(match) >= 0); +} + +export function filterEmojiNamesByAlias(filter) { + return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name))); +} + +let emojiCategoryMap; +export function getEmojiCategoryMap() { + if (!emojiCategoryMap) { + emojiCategoryMap = { + activity: [], + people: [], + nature: [], + food: [], + travel: [], + objects: [], + symbols: [], + flags: [], + }; + Object.keys(emojiMap).forEach((name) => { + const emoji = emojiMap[name]; + if (emojiCategoryMap[emoji.category]) { + emojiCategoryMap[emoji.category].push(name); + } + }); + } + return emojiCategoryMap; +} + +export function getEmojiInfo(query) { + let name = normalizeEmojiName(query); + let emojiInfo = emojiMap[name]; + + // Fallback to question mark for unknown emojis + if (!emojiInfo) { + name = 'grey_question'; + emojiInfo = emojiMap[name]; + } + + return { ...emojiInfo, name }; +} + +export function emojiFallbackImageSrc(inputName) { + const { name, digest } = getEmojiInfo(inputName); + return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`; +} + +export function emojiImageTag(name, src) { + return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; +} + +export function glEmojiTag(inputName, options) { + const opts = { sprite: false, forceFallback: false, ...options }; + const { name, ...emojiInfo } = getEmojiInfo(inputName); + + const fallbackImageSrc = emojiFallbackImageSrc(name); + const fallbackSpriteClass = `emoji-${name}`; + + const classList = []; + if (opts.forceFallback && opts.sprite) { + classList.push('emoji-icon'); + classList.push(fallbackSpriteClass); + } + const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; + const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; + let contents = emojiInfo.moji; + if (opts.forceFallback && !opts.sprite) { + contents = emojiImageTag(name, fallbackImageSrc); + } + + return ` + <gl-emoji + ${classAttribute} + data-name="${name}" + data-fallback-src="${fallbackImageSrc}" + ${fallbackSpriteAttribute} + data-unicode-version="${emojiInfo.unicodeVersion}" + title="${emojiInfo.description}" + > + ${contents} + </gl-emoji> + `; +} diff --git a/app/assets/javascripts/emoji/support/index.js b/app/assets/javascripts/emoji/support/index.js new file mode 100644 index 00000000000..1f7852dd487 --- /dev/null +++ b/app/assets/javascripts/emoji/support/index.js @@ -0,0 +1,10 @@ +import isEmojiUnicodeSupported from './is_emoji_unicode_supported'; +import getUnicodeSupportMap from './unicode_support_map'; + +// cache browser support map between calls +let browserUnicodeSupportMap; + +export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) { + browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap(); + return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion); +} diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js index 4f8884d05ac..3fd23efa9f8 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js @@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe } export { - isEmojiUnicodeSupported, + isEmojiUnicodeSupported as default, isFlagEmoji, isKeycapEmoji, isSkinToneComboEmoji, diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js index 257df55e54f..755381c2f95 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js +++ b/app/assets/javascripts/emoji/support/unicode_support_map.js @@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) { return resultMap; } -function getUnicodeSupportMap() { +export default function getUnicodeSupportMap() { let unicodeSupportMap; let userAgentFromCache; @@ -165,8 +165,3 @@ function getUnicodeSupportMap() { return unicodeSupportMap; } - -export { - getUnicodeSupportMap, - generateUnicodeSupportMap, -}; diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index a2448520a5f..e7495677e7c 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -2,6 +2,7 @@ import playIconSvg from 'icons/_icon_play.svg'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -12,6 +13,10 @@ export default { }, }, + directives: { + tooltip, + }, + components: { loadingIcon, }, @@ -33,8 +38,6 @@ export default { onClickAction(endpoint) { this.isLoading = true; - $(this.$refs.tooltip).tooltip('destroy'); - eventHub.$emit('postAction', endpoint); }, @@ -53,11 +56,11 @@ export default { class="btn-group" role="group"> <button + v-tooltip type="button" - class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" + class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" data-container="body" data-toggle="dropdown" - ref="tooltip" :title="title" :aria-label="title" :disabled="isLoading"> diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index eaeec2bc53c..6b749814ea4 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -1,4 +1,6 @@ <script> +import tooltip from '../../vue_shared/directives/tooltip'; + /** * Renders the external url link in environments table. */ @@ -10,6 +12,10 @@ export default { }, }, + directives: { + tooltip, + }, + computed: { title() { return 'Open'; @@ -19,7 +25,8 @@ export default { </script> <template> <a - class="btn external-url has-tooltip" + v-tooltip + class="btn external-url" data-container="body" target="_blank" rel="noopener noreferrer nofollow" diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index de2269118cd..b25113e0fc6 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue'; import RollbackComponent from './environment_rollback.vue'; import TerminalButtonComponent from './environment_terminal_button.vue'; import MonitoringButtonComponent from './environment_monitoring.vue'; -import CommitComponent from '../../vue_shared/components/commit'; +import CommitComponent from '../../vue_shared/components/commit.vue'; import eventHub from '../event_hub'; /** @@ -403,6 +403,14 @@ export default { return ''; }, + displayEnvironmentActions() { + return this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.hasStopAction || + this.canRetry; + }, + /** * Constructs folder URL based on the current location and the folder id. * @@ -535,10 +543,13 @@ export default { </span> </div> - <div class="table-section section-30 environments-actions table-button-footer" role="gridcell"> + <div + v-if="!model.isFolder && displayEnvironmentActions" + class="table-section section-30 table-button-footer" + role="gridcell"> + <div - v-if="!model.isFolder" - class="btn-group environment-action-buttons" + class="btn-group table-action-buttons" role="group"> <actions-component diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 07cf92281a0..1655561cdd3 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -2,6 +2,8 @@ /** * Renders the Monitoring (Metrics) link in environments table. */ +import tooltip from '../../vue_shared/directives/tooltip'; + export default { props: { monitoringUrl: { @@ -10,6 +12,10 @@ export default { }, }, + directives: { + tooltip, + }, + computed: { title() { return 'Monitoring'; @@ -19,7 +25,8 @@ export default { </script> <template> <a - class="btn monitoring-url has-tooltip hidden-xs hidden-sm" + v-tooltip + class="btn monitoring-url hidden-xs hidden-sm" data-container="body" rel="noopener noreferrer nofollow" :href="monitoringUrl" diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 091c543860b..85f11d2071b 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -5,6 +5,7 @@ */ import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -14,6 +15,10 @@ export default { }, }, + directives: { + tooltip, + }, + data() { return { isLoading: false, @@ -46,8 +51,9 @@ export default { </script> <template> <button + v-tooltip type="button" - class="btn stop-env-link has-tooltip hidden-xs hidden-sm" + class="btn stop-env-link hidden-xs hidden-sm" data-container="body" @click="onClick" :disabled="isLoading" diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 1ca65a79951..2037bf618e3 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -4,6 +4,7 @@ * Used in environments table. */ import terminalIconSvg from 'icons/_icon_terminal.svg'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -14,6 +15,10 @@ export default { }, }, + directives: { + tooltip, + }, + data() { return { terminalIconSvg, @@ -29,7 +34,8 @@ export default { </script> <template> <a - class="btn terminal-button has-tooltip hidden-xs hidden-sm" + v-tooltip + class="btn terminal-button hidden-xs hidden-sm" data-container="body" :title="title" :aria-label="title" diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 8a2f6a473de..a5773dd7e4f 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -158,5 +158,4 @@ export default class EnvironmentsStore { return environments.filter(env => env.isFolder && env.isOpen); } - } diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js new file mode 100644 index 00000000000..dbd3843cef7 --- /dev/null +++ b/app/assets/javascripts/experimental_flags.js @@ -0,0 +1,11 @@ +import Cookies from 'js-cookie'; + +export default () => { + $('.js-experiment-feature-toggle').on('change', (e) => { + const el = e.target; + + Cookies.set(el.name, el.value, { + expires: 365 * 10, + }); + }); +}; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 2af242a69df..5838b1bdbb7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -56,7 +56,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { } renderContent() { - const dropdownData = gl.FilteredSearchTokenKeys.get() + const dropdownData = this.tokenKeys.get() .map(tokenKey => ({ icon: `fa-${tokenKey.icon}`, hint: tokenKey.key, diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 8f547bd8f1f..1425769d2de 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -40,6 +40,10 @@ class FilteredSearchManager { return []; }) .then((searches) => { + if (!searches) { + return; + } + // Put any searches that may have come in before // we fetched the saved searches ahead of the already saved ones const resultantSearches = this.recentSearchesStore.setRecentSearches( @@ -487,6 +491,7 @@ class FilteredSearchManager { } searchState(e) { + e.preventDefault(); const target = e.currentTarget; // remove focus outline after click target.blur(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 401dec1a370..f99bac7da1a 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,8 +1,6 @@ -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; -import { glEmojiTag } from '~/behaviors/gl_emoji'; -import glRegexp from '~/lib/utils/regexp'; -import AjaxCache from '~/lib/utils/ajax_cache'; +import { validEmojiNames, glEmojiTag } from './emoji'; +import glRegexp from './lib/utils/regexp'; +import AjaxCache from './lib/utils/ajax_cache'; function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); @@ -34,7 +32,7 @@ class GfmAutoComplete { const $input = $(input); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); // This triggers at.js again - // Needed for slash commands with suffixes (ex: /label ~) + // Needed for quick actions with suffixes (ex: /label ~) $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); $input.on('clear-commands-cache.atwho', () => this.clearCache()); }); @@ -48,8 +46,8 @@ class GfmAutoComplete { if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.labels) this.setupLabels($input); - // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - $input.filter('[data-supports-slash-commands="true"]').atwho({ + // We don't instantiate the quick actions autocomplete for note and issue/MR edit forms + $input.filter('[data-supports-quick-actions="true"]').atwho({ at: '/', alias: 'commands', searchKey: 'search', @@ -375,7 +373,7 @@ class GfmAutoComplete { if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { - this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); + this.loadData($input, at, validEmojiNames); } else { AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) .then((data) => { diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 32815b9f73e..b1db34b9c50 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -27,7 +27,7 @@ export default { if (this.group.hasSubgroups) { eventHub.$emit('toggleSubGroups', this.group); } else { - window.location.href = this.group.webUrl; + window.location.href = this.group.groupPath; } } }, @@ -192,7 +192,7 @@ export default { <div class="avatar-container s40 hidden-xs"> <a - :href="group.webUrl"> + :href="group.groupPath"> <img class="avatar s40" :src="group.avatarUrl" @@ -202,7 +202,7 @@ export default { <div class="title"> <a - :href="group.webUrl">{{fullPath}}</a> + :href="group.groupPath">{{fullPath}}</a> <template v-if="group.permissions.humanGroupAccess"> as <span class="access-type">{{group.permissions.humanGroupAccess}}</span> diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 67ee7d140ce..6eab6083e8f 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -47,8 +47,8 @@ export default class GroupsStore { // Map groups to an object groups.map((group) => { - mappedGroups[group.id] = group; - mappedGroups[group.id].subGroups = {}; + mappedGroups[`id${group.id}`] = group; + mappedGroups[`id${group.id}`].subGroups = {}; return group; }); @@ -56,26 +56,27 @@ export default class GroupsStore { const currentGroup = mappedGroups[key]; if (currentGroup.parentId) { // If the group is not at the root level, add it to its parent array of subGroups. - const findParentGroup = mappedGroups[currentGroup.parentId]; + const findParentGroup = mappedGroups[`id${currentGroup.parentId}`]; if (findParentGroup) { - mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; - mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups + mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup; + mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups } else if (parentGroup && parentGroup.id === currentGroup.parentId) { - tree[currentGroup.id] = currentGroup; + tree[`id${currentGroup.id}`] = currentGroup; } else { - // Means the groups hast no direct parent. - // Save for later processing, we will add them to its corresponding base group + // No parent found. We save it for later processing orphans.push(currentGroup); + + // Add to tree to preserve original order + tree[`id${currentGroup.id}`] = currentGroup; } } else { - // If the group is at the root level, add it to first level elements array. - tree[currentGroup.id] = currentGroup; + // If the group is at the top level, add it to first level elements array. + tree[`id${currentGroup.id}`] = currentGroup; } return key; }); - // Hopefully this array will be empty for most cases if (orphans.length) { orphans.map((orphan) => { let found = false; @@ -83,11 +84,23 @@ export default class GroupsStore { Object.keys(tree).map((key) => { const group = tree[key]; - if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) { + + if ( + group && + currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 && + // Make sure the currently selected orphan is not the same as the group + // we are checking here otherwise it will end up in an infinite loop + currentOrphan.id !== group.id + ) { group.subGroups[currentOrphan.id] = currentOrphan; group.isOpen = true; currentOrphan.isOrphan = true; found = true; + + // Delete if group was put at the top level. If not the group will be displayed twice. + if (tree[`id${currentOrphan.id}`]) { + delete tree[`id${currentOrphan.id}`]; + } } return key; @@ -95,7 +108,8 @@ export default class GroupsStore { if (!found) { currentOrphan.isOrphan = true; - tree[currentOrphan.id] = currentOrphan; + + tree[`id${currentOrphan.id}`] = currentOrphan; } return orphan; @@ -122,6 +136,7 @@ export default class GroupsStore { canEdit: rawGroup.can_edit, description: rawGroup.description, webUrl: rawGroup.web_url, + groupPath: rawGroup.group_path, parentId: rawGroup.parent_id, visibility: rawGroup.visibility, leavePath: rawGroup.leave_path, @@ -139,7 +154,7 @@ export default class GroupsStore { // eslint-disable-next-line class-methods-use-this removeGroup(group, collection) { - Vue.delete(collection, group.id); + Vue.delete(collection, `id${group.id}`); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 84bd2e092e6..a8856120c5e 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -22,6 +22,7 @@ export default class IssuableBulkUpdateSidebar { initDomElements() { this.$page = $('.page-with-sidebar'); this.$sidebar = $('.right-sidebar'); + this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); this.$bulkEditSubmitBtn = $('.update-selected-issues'); this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle'); @@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar { toggleSidebarDisplay(show) { this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); + this.$sidebarInnerContainer.toggleClass(HIDDEN_CLASS, !show); this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); } diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e14414d3f68..3d5fb7f441c 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -51,6 +51,11 @@ export default { required: false, default: '', }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, updatedAt: { type: String, required: false, @@ -105,6 +110,7 @@ export default { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, }); return { @@ -198,13 +204,7 @@ export default { method: 'getData', successCallback: (res) => { const data = res.json(); - const shouldUpdate = this.store.stateShouldUpdate(data); - this.store.updateState(data); - - if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) { - this.store.formState.lockedWarningVisible = true; - } }, errorCallback(err) { throw new Error(err); diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 5ae617356e0..43db66c8e08 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -37,23 +37,12 @@ }); }, taskStatus() { - const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); - const $issuableHeader = $('.issuable-meta'); - const $tasks = $('#task_status', $issuableHeader); - const $tasksShort = $('#task_status_short', $issuableHeader); - - if (taskRegexMatches) { - $tasks.text(this.taskStatus); - $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); - } else { - $tasks.text(''); - $tasksShort.text(''); - } + this.updateTaskStatusText(); }, }, methods: { renderGFM() { - $(this.$refs['gfm-entry-content']).renderGFM(); + $(this.$refs['gfm-content']).renderGFM(); if (this.canUpdate) { // eslint-disable-next-line no-new @@ -64,9 +53,24 @@ }); } }, + updateTaskStatusText() { + const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); + const $issuableHeader = $('.issuable-meta'); + const $tasks = $('#task_status', $issuableHeader); + const $tasksShort = $('#task_status_short', $issuableHeader); + + if (taskRegexMatches) { + $tasks.text(this.taskStatus); + $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + } else { + $tasks.text(''); + $tasksShort.text(''); + } + }, }, mounted() { this.renderGFM(); + this.updateTaskStatusText(); }, }; </script> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 30a1be5cb50..27b1b814f9a 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -41,13 +41,14 @@ <textarea id="issue-description" class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-slash-commands="false" + data-supports-quick-actionss="false" aria-label="Description" v-model="formState.description" ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." - @keydown.meta.enter="updateIssuable"> + @keydown.meta.enter="updateIssuable" + @keydown.ctrl.enter="updateIssuable"> </textarea> </markdown-field> </div> diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue index f811fb0de24..7bf2be8b28a 100644 --- a/app/assets/javascripts/issue_show/components/fields/project_move.vue +++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue @@ -1,10 +1,10 @@ <script> - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; export default { - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, props: { formState: { type: Object, @@ -71,9 +71,9 @@ data-placeholder="Move to a different project" /> </div> <span + v-tooltip data-placement="auto top" - title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location." - ref="tooltip"> + title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."> <i class="fa fa-question-circle" aria-hidden="true"> diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index 6556bf117e2..83af8e1e245 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -26,6 +26,7 @@ placeholder="Issue title" aria-label="Issue title" v-model="formState.title" - @keydown.meta.enter="updateIssuable" /> + @keydown.meta.enter="updateIssuable" + @keydown.ctrl.enter="updateIssuable" /> </fieldset> </template> diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 14b2a1e18e9..ad8cb6465e2 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + initialTaskStatus: this.initialTaskStatus, }, }); }, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 27c2d349f52..0c8bd6f1cc3 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -1,23 +1,6 @@ export default class Store { - constructor({ - titleHtml, - titleText, - descriptionHtml, - descriptionText, - updatedAt, - updatedByName, - updatedByPath, - }) { - this.state = { - titleHtml, - titleText, - descriptionHtml, - descriptionText, - taskStatus: '', - updatedAt, - updatedByName, - updatedByPath, - }; + constructor(initialState) { + this.state = initialState; this.formState = { title: '', confidential: false, @@ -29,6 +12,10 @@ export default class Store { } updateState(data) { + if (this.stateShouldUpdate(data)) { + this.formState.lockedWarningVisible = true; + } + this.state.titleHtml = data.title; this.state.titleText = data.title_text; this.state.descriptionHtml = data.description; @@ -40,10 +27,8 @@ export default class Store { } stateShouldUpdate(data) { - return { - title: this.state.titleText !== data.title_text, - description: this.state.descriptionText !== data.description_text, - }; + return this.state.titleText !== data.title_text || + this.state.descriptionText !== data.description_text; } setFormState(state) { diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 38b2eb9ff14..d8814802d9e 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -21,6 +21,7 @@ } bindEvents() { + this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick); return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); } @@ -36,6 +37,11 @@ _this.toggleEmptyState($label, $btn, action); } + onButtonActionClick(e) { + e.stopPropagation(); + $(e.currentTarget).tooltip('hide'); + } + toggleEmptyState($label, $btn, action) { this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2aca86189fd..122ec138c59 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -86,18 +86,25 @@ // This is required to handle non-unicode characters in hash hash = decodeURIComponent(hash); + var fixedTabs = document.querySelector('.js-tabs-affix'); + var fixedNav = document.querySelector('.navbar-gitlab'); + + var adjustment = 0; + if (fixedNav) adjustment -= fixedNav.offsetHeight; + // scroll to user-generated markdown anchor if we cannot find a match if (document.getElementById(hash) === null) { var target = document.getElementById('user-content-' + hash); if (target && target.scrollIntoView) { target.scrollIntoView(true); + window.scrollBy(0, adjustment); } } else { // only adjust for fixedTabs when not targeting user-generated content - var fixedTabs = document.querySelector('.js-tabs-affix'); if (fixedTabs) { - window.scrollBy(0, -fixedTabs.offsetHeight); + adjustment -= fixedTabs.offsetHeight; } + window.scrollBy(0, adjustment); } }; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 54c0da3fc9c..bfcc50996cc 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -34,7 +34,7 @@ window.dateFormat = dateFormat; w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) { $timeagoEls.each((i, el) => { - el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime'))); + el.setAttribute('title', el.getAttribute('title')); if (setTimeago) { // Recreate with custom template diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 601d01e1be1..021f936a4fa 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -94,8 +94,8 @@ gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; - if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { - if (blockTag != null) { + if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { + if (blockTag != null && blockTag !== '') { insertText = this.blockTagText(text, textArea, blockTag, selected); } else { insertText = selectedSplit.map(function(val) { diff --git a/app/assets/javascripts/locale/bg/app.js b/app/assets/javascripts/locale/bg/app.js deleted file mode 100644 index ba56c0bea25..00000000000 --- a/app/assets/javascripts/locale/bg/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['bg'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 09:40-0400","Last-Translator":"Lyubomir Vasilev <lyubomirv@abv.bg>","Language-Team":"Bulgarian","Language":"bg","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"bg","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["от"],"Commit":["Подаване","Подавания"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Анализът на циклите дава общ поглед върху това колко време е нужно на една идея да се превърне в завършена функционалност в проекта."],"CycleAnalyticsStage|Code":["Програмиране"],"CycleAnalyticsStage|Issue":["Проблем"],"CycleAnalyticsStage|Plan":["Планиране"],"CycleAnalyticsStage|Production":["Издаване"],"CycleAnalyticsStage|Review":["Преглед и одобрение"],"CycleAnalyticsStage|Staging":["Подготовка за издаване"],"CycleAnalyticsStage|Test":["Тестване"],"Deploy":["Внедряване","Внедрявания"],"FirstPushedBy|First":["Първо"],"FirstPushedBy|pushed by":["изпращане на промени от"],"From issue creation until deploy to production":["От създаването на проблема до внедряването в крайната версия"],"From merge request merge until deploy to production":["От прилагането на заявката за сливане до внедряването в крайната версия"],"Introducing Cycle Analytics":["Представяме Ви анализът на циклите"],"Last %d day":["Последния %d ден","Последните %d дни"],"Limited to showing %d event at most":["Ограничено до показване на последното %d събитие","Ограничено до показване на последните %d събития"],"Median":["Медиана"],"New Issue":["Нов проблем","Нови проблема"],"Not available":["Не е налично"],"Not enough data":["Няма достатъчно данни"],"OpenedNDaysAgo|Opened":["Отворен"],"Pipeline Health":["Състояние"],"ProjectLifecycle|Stage":["Етап"],"Read more":["Прочетете повече"],"Related Commits":["Свързани подавания"],"Related Deployed Jobs":["Свързани задачи за внедряване"],"Related Issues":["Свързани проблеми"],"Related Jobs":["Свързани задачи"],"Related Merge Requests":["Свързани заявки за сливане"],"Related Merged Requests":["Свързани приложени заявки за сливане"],"Showing %d event":["Показване на %d събитие","Показване на %d събития"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Етапът на програмиране показва времето от първото подаване до създаването на заявката за сливане. Данните ще бъдат добавени тук автоматично след като бъде създадена първата заявка за сливане."],"The collection of events added to the data gathered for that stage.":["Съвкупността от събития добавени към данните събрани за този етап."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Етапът на проблемите показва колко е времето от създаването на проблем до определянето на целеви етап на проекта за него, или до добавянето му в списък на дъската за проблеми. Започнете да добавяте проблеми, за да видите данните за този етап."],"The phase of the development lifecycle.":["Етапът от цикъла на разработка"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Етапът на планиране показва колко е времето от преходната стъпка до изпращането на първото подаване. Това време ще бъде добавено автоматично след като изпратите първото си подаване."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Етапът на издаване показва общото време, което е нужно от създаването на проблем до внедряването на кода в крайната версия."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Етапът на преглед и одобрение показва времето от създаването на заявката за сливане до прилагането ѝ. Данните ще бъдат добавени автоматично след като приложите първата си заявка за сливане."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Етапът на подготовка за издаване показва времето между прилагането на заявката за сливане и внедряването на кода в средата на работещата крайна версия. Данните ще бъдат добавени автоматично след като направите първото си внедряване в крайната версия."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени автоматично след като приключи изпълнените на първата Ви такава задача."],"The time taken by each data entry gathered by that stage.":["Времето, което отнема всеки запис от данни за съответния етап."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Стойността, която се намира в средата на последователността от наблюдавани данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е (5+7)/2 = 6."],"Time before an issue gets scheduled":["Време преди един проблем да бъде планиран за работа"],"Time before an issue starts implementation":["Време преди работата по проблем да започне"],"Time between merge request creation and merge/close":["Време между създаване на заявка за сливане и прилагането/отхвърлянето ѝ"],"Time until first merge request":["Време преди първата заявка за сливане"],"Time|hr":["час","часа"],"Time|min":["мин","мин"],"Time|s":["сек"],"Total Time":["Общо време"],"Total test time for all commits/merges":["Общо време за тестване на всички подавания/сливания"],"Want to see the data? Please ask an administrator for access.":["Искате ли да видите данните? Помолете администратор за достъп."],"We don't have enough data to show this stage.":["Няма достатъчно данни за този етап."],"You need permission.":["Нуждаете се от разрешение."],"day":["ден","дни"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/de/app.js b/app/assets/javascripts/locale/de/app.js deleted file mode 100644 index e7d2b174405..00000000000 --- a/app/assets/javascripts/locale/de/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-09 13:44+0200","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["Von"],"Cancel":[""],"Commit":["Commit","Commits"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Issue"],"CycleAnalyticsStage|Plan":["Planung"],"CycleAnalyticsStage|Production":["Produktiv"],"CycleAnalyticsStage|Review":["Review"],"CycleAnalyticsStage|Staging":["Staging"],"CycleAnalyticsStage|Test":["Test"],"Delete":[""],"Deploy":["Deployment","Deployments"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["Erster"],"FirstPushedBy|pushed by":["gepusht von"],"From issue creation until deploy to production":["Vom Anlegen des Issues bis zum Produktivdeployment"],"From merge request merge until deploy to production":["Vom Merge Request bis zum Produktivdeployment"],"Interval Pattern":[""],"Introducing Cycle Analytics":["Was sind Cycle Analytics?"],"Last %d day":["Letzter %d Tag","Letzten %d Tage"],"Last Pipeline":[""],"Limited to showing %d event at most":["Eingeschränkt auf maximal %d Ereignis","Eingeschränkt auf maximal %d Ereignisse"],"Median":["Median"],"New Issue":["Neues Issue","Neue Issues"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["Nicht verfügbar"],"Not enough data":["Nicht genügend Daten"],"OpenedNDaysAgo|Opened":["Erstellt"],"Owner":[""],"Pipeline Health":["Pipeline Kennzahlen"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["Phase"],"Read more":["Mehr"],"Related Commits":["Zugehörige Commits"],"Related Deployed Jobs":["Zugehörige Deploymentjobs"],"Related Issues":["Zugehörige Issues"],"Related Jobs":["Zugehörige Jobs"],"Related Merge Requests":["Zugehörige Merge Requests"],"Related Merged Requests":["Zugehörige abgeschlossene Merge Requests"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["Zeige %d Ereignis","Zeige %d Ereignisse"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt."],"The collection of events added to the data gathered for that stage.":["Ereignisse, die für diese Phase ausgewertet wurden."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."],"The phase of the development lifecycle.":["Die Phase im Entwicklungsprozess."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."],"The time taken by each data entry gathered by that stage.":["Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."],"Time before an issue gets scheduled":["Zeit bis ein Issue geplant wird"],"Time before an issue starts implementation":["Zeit bis die Implementierung für ein Issue beginnt"],"Time between merge request creation and merge/close":["Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"],"Time until first merge request":["Zeit bis zum ersten Merge Request"],"Time|hr":["h","h"],"Time|min":["min","min"],"Time|s":["s"],"Total Time":["Gesamtzeit"],"Total test time for all commits/merges":["Gesamte Testlaufzeit für alle Commits/Merges"],"Want to see the data? Please ask an administrator for access.":["Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."],"We don't have enough data to show this stage.":["Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."],"You need permission.":["Sie benötigen Zugriffsrechte."],"day":["Tag","Tage"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/en/app.js b/app/assets/javascripts/locale/en/app.js deleted file mode 100644 index 0bb76c80b7a..00000000000 --- a/app/assets/javascripts/locale/en/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":[""],"Cancel":[""],"Commit":["",""],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Last Pipeline":[""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Owner":[""],"Pipeline Health":[""],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["",""],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/eo/app.js b/app/assets/javascripts/locale/eo/app.js new file mode 100644 index 00000000000..55f000e9b88 --- /dev/null +++ b/app/assets/javascripts/locale/eo/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['eo'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-06-15 21:59-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-20 06:24-0400","Last-Translator":"Lyubomir Vasilev <lyubomirv@abv.bg>","Language-Team":"Esperanto (https://translate.zanata.org/project/view/GitLab)","Language":"eo","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"eo","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} enmetis %{commit_timeago}"],"About auto deploy":["Pri la aŭtomata disponigado"],"Active":["Aktiva"],"Activity":["Aktiveco"],"Add Changelog":["Aldoni liston de ŝanĝoj"],"Add Contribution guide":["Aldoni gvidliniojn por kontribuado"],"Add License":["Aldoni rajtigilon"],"Add an SSH key to your profile to pull or push via SSH.":["Aldonu SSH-ŝlosilon al via profilo por ebligi al vi eltiri kaj alpuŝi per SSH."],"Add new directory":["Aldoni novan dosierujon"],"Archived project! Repository is read-only":["Arkivita projekto! La deponejo permesas nur legadon"],"Are you sure you want to delete this pipeline schedule?":["Ĉu vi certe volas forigi ĉi tiun ĉenstablan planon?"],"Attach a file by drag & drop or %{upload_link}":["Alkroĉu dosieron per ŝovmetado aŭ %{upload_link}"],"Branch":["Branĉo","Branĉoj"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aŭtomatan disponigadon, bonvolu elekti Yaml-ŝablonon por GitLab CI kaj enmeti viajn ŝanĝojn. %{link_to_autodeploy_doc}"],"Branches":["Branĉoj"],"Browse files":["Elekti dosierojn"],"ByAuthor|by":["de"],"CI configuration":["Agordoj de seninterrompa integrado"],"Cancel":["Nuligi"],"ChangeTypeActionLabel|Pick into branch":["Elekti en branĉon"],"ChangeTypeActionLabel|Revert in branch":["Malfari en branĉo"],"ChangeTypeAction|Cherry-pick":["Precize elekti"],"ChangeTypeAction|Revert":["Malfari"],"Changelog":["Listo de ŝanĝoj"],"Charts":["Diagramoj"],"Cherry-pick this commit":["Precize elekti ĉi tiun kunmetadon"],"Cherry-pick this merge request":["Precize elekti ĉi tiun peton pri kunfando"],"CiStatusLabel|canceled":["nuligita"],"CiStatusLabel|created":["kreita"],"CiStatusLabel|failed":["malsukcesa"],"CiStatusLabel|manual action":["mana ago"],"CiStatusLabel|passed":["sukcesa"],"CiStatusLabel|passed with warnings":["sukcesa, kun avertoj"],"CiStatusLabel|pending":["okazonta"],"CiStatusLabel|skipped":["transsaltita"],"CiStatusLabel|waiting for manual action":["atendanta manan agon"],"CiStatusText|blocked":["blokita"],"CiStatusText|canceled":["nuligita"],"CiStatusText|created":["kreita"],"CiStatusText|failed":["malsukcesa"],"CiStatusText|manual":["mana"],"CiStatusText|passed":["sukcesa"],"CiStatusText|pending":["okazonta"],"CiStatusText|skipped":["transsaltita"],"CiStatus|running":["plenumiĝanta"],"Commit":["Enmetado","Enmetadoj"],"Commit message":["Mesaĝo pri la enmetado"],"CommitBoxTitle|Commit":["Enmeti"],"CommitMessage|Add %{file_name}":["Aldoni „%{file_name}“"],"Commits":["Enmetadoj"],"Commits|History":["Historio"],"Committed by":["Enmetita de"],"Compare":["Kompari"],"Contribution guide":["Gvidlinioj por kontribuado"],"Contributors":["Kontribuantoj"],"Copy URL to clipboard":["Kopii la adreson en la kopibufron"],"Copy commit SHA to clipboard":["Kopii la identigilon de la enmetado"],"Create New Directory":["Krei novan dosierujon"],"Create directory":["Krei dosierujon"],"Create empty bare repository":["Krei malplenan deponejon"],"Create merge request":["Krei peton pri kunfando"],"Create new...":["Krei novan…"],"CreateNewFork|Fork":["Disbranĉigi"],"CreateTag|Tag":["Etikedo"],"Cron Timezone":["Horzono por Cron"],"Cron syntax":["La sintakso de Cron"],"Custom notification events":["Propraj sciigaj eventoj"],"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 %{notification_link}.":["La propraj sciigaj niveloj estas la samaj kiel la niveloj de partoprenado. Uzante la proprajn sciigajn nivelojn, vi ricevos ankaŭ sciigojn por elektitaj de vi eventoj. Por lerni pli, bonvolu vidi %{notification_link}."],"Cycle Analytics":["Cikla analizo"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["La cikla analizo esploras kiom da tempo necesas por disvolvi ideon ĝis ĝi fariĝos realaĵo."],"CycleAnalyticsStage|Code":["Programado"],"CycleAnalyticsStage|Issue":["Problemo"],"CycleAnalyticsStage|Plan":["Plano"],"CycleAnalyticsStage|Production":["Eldonado"],"CycleAnalyticsStage|Review":["Kontrolo"],"CycleAnalyticsStage|Staging":["Preparo por eldono"],"CycleAnalyticsStage|Test":["Testado"],"Define a custom pattern with cron syntax":["Difini propran ŝablonon, uzante la sintakson de Cron"],"Delete":["Forigi"],"Deploy":["Disponigado","Disponigadoj"],"Description":["Priskribo"],"Directory name":["Nomo de dosierujo"],"Don't show again":["Ne montru denove"],"Download":["Elŝuti"],"Download tar":["Elŝuti en formato „tar“"],"Download tar.bz2":["Elŝuti en formato „tar.bz2“"],"Download tar.gz":["Elŝuti en formato „tar.gz“"],"Download zip":["Elŝuti en formato „zip“"],"DownloadArtifacts|Download":["Elŝuti"],"DownloadCommit|Email Patches":["Sendi flikaĵojn per retpoŝto"],"DownloadCommit|Plain Diff":["Normala dosiero kun diferencoj"],"DownloadSource|Download":["Elŝuti"],"Edit":["Redakti"],"Edit Pipeline Schedule %{id}":["Redakti ĉenstablan planon %{id}"],"Every day (at 4:00am)":["Ĉiutage (je 4:00)"],"Every month (on the 1st at 4:00am)":["Ĉiumonate (en la 1a de la monato, je 4:00)"],"Every week (Sundays at 4:00am)":["Ĉiusemajne (en dimanĉo, je 4:00)"],"Failed to change the owner":["Ne eblas ŝanĝi la posedanton"],"Failed to remove the pipeline schedule":["Ne eblas forigi la ĉenstablan planon"],"Files":["Dosieroj"],"Find by path":["Trovi per dosierindiko"],"Find file":["Trovi dosieron"],"FirstPushedBy|First":["Unue"],"FirstPushedBy|pushed by":["alpuŝita de"],"Fork":["Disbranĉigo","Disbranĉigoj"],"ForkedFromProjectPath|Forked from":["Disbranĉigita el"],"From issue creation until deploy to production":["De la kreado de la problemo ĝis la disponigado en la publika versio"],"From merge request merge until deploy to production":["De la kunfandado de la peto pri kunfando ĝis la disponigado en la publika versio"],"Go to your fork":["Al via disbranĉigo"],"GoToYourFork|Fork":["Disbranĉigo"],"Home":["Hejmo"],"Housekeeping successfully started":["La refreŝigo komenciĝis sukcese"],"Import repository":["Enporti deponejon"],"Interval Pattern":["Intervala ŝablono"],"Introducing Cycle Analytics":["Ni prezentas al vi la ciklan analizon"],"LFSStatus|Disabled":["Malŝaltita"],"LFSStatus|Enabled":["Ŝaltita"],"Last %d day":["La lasta %d tago","La lastaj %d tagoj"],"Last Pipeline":["Lasta ĉenstablo"],"Last Update":["Lasta ĝisdatigo"],"Last commit":["Lasta enmetado"],"Learn more in the":["Lernu pli en la"],"Learn more in the|pipeline schedules documentation":["dokumentado pri ĉenstablaj planoj"],"Leave group":["Forlasi la grupon"],"Leave project":["Forlasi la projekton"],"Limited to showing %d event at most":["Limigita al montrado de ne pli ol %d evento","Limigita al montrado de ne pli ol %d eventoj"],"Median":["Mediano"],"MissingSSHKeyWarningLink|add an SSH key":["aldonos SSH-ŝlosilon"],"New Issue":["Nova problemo","Novaj problemoj"],"New Pipeline Schedule":["Nova ĉenstabla plano"],"New branch":["Nova branĉo"],"New directory":["Nova dosierujo"],"New file":["Nova dosiero"],"New issue":["Nova problemo"],"New merge request":["Nova peto pri kunfando"],"New schedule":["Nova plano"],"New snippet":["Nova kodaĵo"],"New tag":["Nova etikedo"],"No repository":["Ne estas deponejo"],"No schedules":["Ne estas planoj"],"Not available":["Ne disponebla"],"Not enough data":["Ne estas sufiĉe da datenoj"],"Notification events":["Sciigaj eventoj"],"NotificationEvent|Close issue":["Fermi problemon"],"NotificationEvent|Close merge request":["Fermi peton pri kunfando"],"NotificationEvent|Failed pipeline":["Malsukcesa ĉenstablo"],"NotificationEvent|Merge merge request":["Apliki peton pri kunfando"],"NotificationEvent|New issue":["Nova problemo"],"NotificationEvent|New merge request":["Nova peto pri kunfando"],"NotificationEvent|New note":["Nova noto"],"NotificationEvent|Reassign issue":["Reatribui problemon"],"NotificationEvent|Reassign merge request":["Reatribui peton pri kunfando"],"NotificationEvent|Reopen issue":["Remalfermi problemon"],"NotificationEvent|Successful pipeline":["Sukcesa ĉenstablo"],"NotificationLevel|Custom":["Propraj"],"NotificationLevel|Disabled":["Malŝaltitaj"],"NotificationLevel|Global":["Ĝeneralaj"],"NotificationLevel|On mention":["Ĉe mencio"],"NotificationLevel|Participate":["Partoprenado"],"NotificationLevel|Watch":["Rigardado"],"OfSearchInADropdown|Filter":["Filtrilo"],"OpenedNDaysAgo|Opened":["Malfermita"],"Options":["Opcioj"],"Owner":["Posedanto"],"Pipeline":["Ĉenstablo"],"Pipeline Health":["Stato"],"Pipeline Schedule":["Ĉenstabla plano"],"Pipeline Schedules":["Ĉenstablaj planoj"],"PipelineSchedules|Activated":["Ŝaltita"],"PipelineSchedules|Active":["Ŝaltitaj"],"PipelineSchedules|All":["Ĉiuj"],"PipelineSchedules|Inactive":["Malŝaltitaj"],"PipelineSchedules|Next Run":["Sekvanta plenumo"],"PipelineSchedules|None":["Nenio"],"PipelineSchedules|Provide a short description for this pipeline":["Entajpu mallongan priskribon pri ĉi tiu ĉenstablo"],"PipelineSchedules|Take ownership":["Akiri posedon"],"PipelineSchedules|Target":["Celo"],"PipelineSheduleIntervalPattern|Custom":["Propra"],"Pipeline|with stage":["kun etapo"],"Pipeline|with stages":["kun etapoj"],"Project '%{project_name}' queued for deletion.":["La projekto „%{project_name}“ estis alvicigita por forigado."],"Project '%{project_name}' was successfully created.":["La projekto „%{project_name}“ estis sukcese kreita."],"Project '%{project_name}' was successfully updated.":["La projekto „%{project_name}“ estis sukcese ĝisdatigita."],"Project '%{project_name}' will be deleted.":["La projekto „%{project_name}“ estos forigita."],"Project access must be granted explicitly to each user.":["Ĉiu uzanto devas akiri propran atingon al la projekto."],"Project export could not be deleted.":["Ne eblas forigi la projektan elporton."],"Project export has been deleted.":["La projekta elporto estis forigita."],"Project export link has expired. Please generate a new export from your project settings.":["La ligilo por la projekta elporto eksvalidiĝis. Bonvolu krei novan elporton en la agordoj de la projekto."],"Project export started. A download link will be sent by email.":["La elporto de la projekto komenciĝis. Vi ricevos ligilon per retpoŝto por elŝuti la datenoj."],"Project home":["Hejmo de la projekto"],"ProjectFeature|Disabled":["Malŝaltita"],"ProjectFeature|Everyone with access":["Ĉiu, kiu havas atingon"],"ProjectFeature|Only team members":["Nur skipanoj"],"ProjectFileTree|Name":["Nomo"],"ProjectLastActivity|Never":["Neniam"],"ProjectLifecycle|Stage":["Etapo"],"ProjectNetworkGraph|Graph":["Grafeo"],"Read more":["Legu pli"],"Readme":["LeguMin"],"RefSwitcher|Branches":["Branĉoj"],"RefSwitcher|Tags":["Etikedoj"],"Related Commits":["Rilataj enmetadoj"],"Related Deployed Jobs":["Rilataj disponigitaj taskoj"],"Related Issues":["Rilataj problemoj"],"Related Jobs":["Rilataj taskoj"],"Related Merge Requests":["Rilataj petoj pri kunfando"],"Related Merged Requests":["Rilataj aplikitaj petoj pri kunfando"],"Remind later":["Rememorigu denove"],"Remove project":["Forigi la projekton"],"Request Access":["Peti atingeblon"],"Revert this commit":["Malfari ĉi tiun enmetadon"],"Revert this merge request":["Malfari ĉi tiun peton pri kunfando"],"Save pipeline schedule":["Konservi ĉenstablan planon"],"Schedule a new pipeline":["Plani novan ĉenstablon"],"Scheduling Pipelines":["Planado de la ĉenstabloj"],"Search branches and tags":["Serĉu branĉon aŭ etikedon"],"Select Archive Format":["Elektu formaton de arkivo"],"Select a timezone":["Elektu horzonon"],"Select target branch":["Elektu celan branĉon"],"Set a password on your account to pull or push via %{protocol}":["Kreu pasvorton por via konto por ebligi al vi eltiri kaj alpuŝi per %{protocol}"],"Set up CI":["Agordi SI"],"Set up Koding":["Agordi „Koding“"],"Set up auto deploy":["Agordi aŭtomatan disponigadon"],"SetPasswordToCloneLink|set a password":["kreos pasvorton"],"Showing %d event":["Estas montrata %d evento","Estas montrataj %d eventoj"],"Source code":["Kodo"],"StarProject|Star":["Steligi"],"Start a %{new_merge_request} with these changes":["Kreu %{new_merge_request} kun ĉi tiuj ŝanĝoj"],"Switch branch/tag":["Iri al branĉo/etikedo"],"Tag":["Etikedo","Etikedoj"],"Tags":["Etikedoj"],"Target Branch":["Cela branĉo"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapo de programado montras la tempon de la unua enmetado ĝis la kreado de la peto pri kunfando. La datenoj aldoniĝos aŭtomate ĉi tie post kiam vi kreas la unuan peton pri kunfando."],"The collection of events added to the data gathered for that stage.":["La aro da eventoj, kiuj estas aldonitaj al la datenoj kolektitaj por la etapo."],"The fork relationship has been removed.":["La rilato de disbranĉigo estis forigita."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapo de la problemo montras kiom la tempo pasas de la kreado de problemo ĝis la atribuado de la problemo al cela etapo de la projekto, aŭ al listo sur la problemtabulo. Komencu krei problemojn por vidi la datenojn por ĉi tiu etapo."],"The phase of the development lifecycle.":["La etapo de la disvolva ciklo."],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["La ĉenstabla plano plenumas ĉenstablojn en la estonteco, ripete, por difinitaj branĉoj aŭ etikedoj. Tiuj planitaj ĉenstabloj heredos la limigitan atingon al la projekto de la rilata uzanto."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapo de la plano montras la tempon de la antaŭa ŝtupo ĝis la alpuŝado de via unua enmetado. Ĉi tiu tempo aldoniĝos aŭtomate post kiam vi alpuŝas la unuan enmetadon."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapo de eldonado montras la tutan tempon de la kreado de problemo ĝis la disponigado en la publika versio. La datenoj aldoniĝos aŭtomate post kiam vi kompletigos plenan ciklon de ideo ĝis realaĵo."],"The project can be accessed by any logged in user.":["Ĉiu ensalutita uzanto havas atingon al la projekto"],"The project can be accessed without any authentication.":["Ĉiu povas havi atingon al la projekto, sen ensaluti"],"The repository for this project does not exist.":["La deponejo por ĉi tiu projekto ne ekzistas."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapo de la kontrolo montras la tempon de la kreado de la peto pri kunfando ĝis ĝia aplikado. La datenoj aldoniĝos aŭtomate post kiam vi aplikos la unuan peton pri kunfando."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapo de preparo por eldono montras la tempon inter la aplikado de la peto pri kunfando kaj la disponigado de la kodo en la publika versio. La datenoj aldoniĝos aŭtomate post kiam vi faros la unuan disponigadon en la publika versio."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapo de testado montras kiom da tempo necesas al „GitLab CI“ por plenumi ĉiujn ĉenstablojn por la rilata peto pri kunfando. La datenoj aldoniĝos aŭtomate post kiam via unua ĉenstablo finiĝos."],"The time taken by each data entry gathered by that stage.":["La tempo, kiu estas necesa por ĉiu dateno kolektita de la etapo."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["La valoro, kiu troviĝas en la mezo de aro da rigardataj valoroj. Ekzemple: inter 3, 5 kaj 9, la mediano estas 5. Inter 3, 5, 7 kaj 8, la mediano estas (5+7)/2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Ĉi tiu signifas, ke vi ne povos alpuŝi kodon, antaŭ ol vi kreos malplenan deponejon aŭ enportos jam ekzistantan."],"Time before an issue gets scheduled":["Tempo antaŭ problemo estas planita por ellabori"],"Time before an issue starts implementation":["Tempo antaŭ la komenco de laboro super problemo"],"Time between merge request creation and merge/close":["Tempo inter la kreado de poeto pri kunfando kaj ĝia aplikado/fermado"],"Time until first merge request":["Tempo ĝis la unua peto pri kunfando"],"Timeago|%s days ago":["antaŭ %s tagoj"],"Timeago|%s days remaining":["restas %s tagoj"],"Timeago|%s hours remaining":["restas %s horoj"],"Timeago|%s minutes ago":["antaŭ %s minutoj"],"Timeago|%s minutes remaining":["restas %s minutoj"],"Timeago|%s months ago":["antaŭ %s monatoj"],"Timeago|%s months remaining":["restas %s monatoj"],"Timeago|%s seconds remaining":["restas %s sekundoj"],"Timeago|%s weeks ago":["antaŭ %s semajnoj"],"Timeago|%s weeks remaining":["restas %s semajnoj"],"Timeago|%s years ago":["antaŭ %s jaroj"],"Timeago|%s years remaining":["restas %s jaroj"],"Timeago|1 day remaining":["restas 1 tago"],"Timeago|1 hour remaining":["restas 1 horo"],"Timeago|1 minute remaining":["restas 1 minuto"],"Timeago|1 month remaining":["restas 1 monato"],"Timeago|1 week remaining":["restas 1 semajno"],"Timeago|1 year remaining":["restas 1 jaro"],"Timeago|Past due":["Malfruiĝis"],"Timeago|a day ago":["antaŭ unu tago"],"Timeago|a month ago":["antaŭ unu monato"],"Timeago|a week ago":["antaŭ unu semajno"],"Timeago|a while":["antaŭ iom da tempo"],"Timeago|a year ago":["antaŭ unu jaro"],"Timeago|about %s hours ago":["antaŭ ĉirkaŭ %s horoj"],"Timeago|about a minute ago":["antaŭ ĉirkaŭ unu minuto"],"Timeago|about an hour ago":["antaŭ ĉirkaŭ unu horo"],"Timeago|in %s days":["post %s tagoj"],"Timeago|in %s hours":["post %s horoj"],"Timeago|in %s minutes":["post %s minutoj"],"Timeago|in %s months":["post %s monatoj"],"Timeago|in %s seconds":["post %s sekundoj"],"Timeago|in %s weeks":["post %s semajnoj"],"Timeago|in %s years":["post %s jaroj"],"Timeago|in 1 day":["post 1 tago"],"Timeago|in 1 hour":["post 1 horo"],"Timeago|in 1 minute":["post 1 minuto"],"Timeago|in 1 month":["post 1 monato"],"Timeago|in 1 week":["post 1 semajno"],"Timeago|in 1 year":["post 1 jaro"],"Timeago|less than a minute ago":["antaŭ malpli ol minuto"],"Time|hr":["h","h"],"Time|min":["min","min"],"Time|s":["s"],"Total Time":["Totala tempo"],"Total test time for all commits/merges":["Totala tempo por la testado de ĉiuj enmetadoj/kunfandoj"],"Unstar":["Malsteligi"],"Upload New File":["Alŝuti novan dosieron"],"Upload file":["Alŝuti dosieron"],"Use your global notification setting":["Uzi vian ĝeneralan agordon pri la sciigoj"],"VisibilityLevel|Internal":["Interna"],"VisibilityLevel|Private":["Privata"],"VisibilityLevel|Public":["Publika"],"Want to see the data? Please ask an administrator for access.":["Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto."],"We don't have enough data to show this stage.":["Ne estas sufiĉe da datenoj por montri ĉi tiun etapon."],"Withdraw Access Request":["Nuligi la peton pri atingeblo"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Vi forigos „%{project_name_with_namespace}“.\\nOni NE POVAS malfari la forigon de projekto!\\nĈu vi estas ABSOLUTE certa?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vi forigos la rilaton de la disbranĉigo al la originala projekto, „%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vi transigos „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas ABSOLUTE certa?"],"You can only add files when you are on a branch":["Oni povas aldoni dosierojn nur kiam oni estas en branĉo"],"You have reached your project limit":["Vi ne povas krei pliajn projektojn"],"You must sign in to star a project":["Oni devas ensaluti por steligi projekton"],"You need permission.":["VI bezonas permeson."],"You will not get any notifications via email":["VI ne ricevos sciigojn per retpoŝto"],"You will only receive notifications for the events you choose":["Vi ricevos sciigojn nur por la eventoj elektitaj de vi"],"You will only receive notifications for threads you have participated in":["Vi ricevos sciigojn nur por la fadenoj, en kiuj vi partoprenis"],"You will receive notifications for any activity":["Vi ricevos sciigojn por ĉiu ago"],"You will receive notifications only for comments in which you were @mentioned":["Vi ricevos sciigojn nur por komentoj, en kiuj vi estas @menciita"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["Vi ne povos eltiri aŭ alpuŝi kodon per %{protocol} antaŭ ol vi %{set_password_link} por via konto"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["Vi ne povos eltiri aŭ alpuŝi kodon per SSH antaŭ ol vi %{add_ssh_key_link} al via profilo"],"Your name":["Via nomo"],"day":["tago","tagoj"],"new merge request":["novan peton pri kunfando"],"notification emails":["sciigoj per retpoŝto"],"parent":["patro","patroj"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js deleted file mode 100644 index 6977625f4d8..00000000000 --- a/app/assets/javascripts/locale/es/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-07 12:29-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"About auto deploy":["Acerca del auto despliegue"],"Activity":["Actividad"],"Add Changelog":["Agregar Changelog"],"Add Contribution guide":["Agregar guía de contribución"],"Add License":["Agregar Licencia"],"Add an SSH key to your profile to pull or push via SSH.":["Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."],"Add new directory":["Agregar nuevo directorio"],"Archived project! Repository is read-only":["¡Proyecto archivado! El repositorio es de sólo lectura"],"Branch":["Rama","Ramas"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"],"Branches":["Ramas"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Changelog":["Changelog"],"Charts":["Gráficos"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallado"],"CiStatusLabel|manual action":["acción manual"],"CiStatusLabel|passed":["pasó"],"CiStatusLabel|passed with warnings":["pasó con advertencias"],"CiStatusLabel|pending":["pendiente"],"CiStatusLabel|skipped":["omitido"],"CiStatusLabel|waiting for manual action":["esperando acción manual"],"CiStatusText|blocked":["bloqueado"],"CiStatusText|canceled":["cancelado"],"CiStatusText|created":["creado"],"CiStatusText|failed":["fallado"],"CiStatusText|manual":["manual"],"CiStatusText|passed":["pasó"],"CiStatusText|pending":["pendiente"],"CiStatusText|skipped":["omitido"],"CiStatus|running":["en ejecución"],"Commit":["Cambio","Cambios"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits|History":["Historial"],"Compare":["Comparar"],"Contribution guide":["Guía de contribución"],"Contributors":["Contribuidores"],"Copy URL to clipboard":["Copiar URL al portapapeles"],"Copy commit SHA to clipboard":["Copiar SHA del cambio al portapapeles"],"Create New Directory":["Crear Nuevo Directorio"],"Create directory":["Crear directorio"],"Create empty bare repository":["Crear repositorio vacío"],"Create merge request":["Crear solicitud de fusión"],"CreateNewFork|Fork":["Bifurcar"],"Custom notification events":["Eventos de notificaciones personalizadas"],"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 %{notification_link}.":["Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."],"Cycle Analytics":["Cycle Analytics"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadSource|Download":["Descargar"],"Files":["Archivos"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"Forks":["Bifurcaciones"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Go to your fork":["Ir a tu bifurcación"],"GoToYourFork|Fork":["Bifurcación"],"Home":["Inicio"],"Housekeeping successfully started":["Servicio de limpieza iniciado con éxito"],"Import repository":["Importar repositorio"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"LFSStatus|Disabled":["Deshabilitado"],"LFSStatus|Enabled":["Habilitado"],"Last %d day":["Último %d día","Últimos %d días"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Leave group":["Abandonar grupo"],"Leave project":["Abandonar proyecto"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"MissingSSHKeyWarningLink|add an SSH key":["agregar una clave SSH"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"New branch":["Nueva rama"],"New directory":["Nuevo directorio"],"New file":["Nuevo archivo"],"New issue":["Nueva incidencia"],"New merge request":["Nueva solicitud de fusión"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"Notification events":["Eventos de notificación"],"NotificationEvent|Close issue":["Cerrar incidencia"],"NotificationEvent|Close merge request":["Cerrar solicitud de fusión"],"NotificationEvent|Failed pipeline":["Pipeline fallido"],"NotificationEvent|Merge merge request":["Integrar solicitud de fusión"],"NotificationEvent|New issue":["Nueva incidencia"],"NotificationEvent|New merge request":["Nueva solicitud de fusión"],"NotificationEvent|New note":["Nueva nota"],"NotificationEvent|Reassign issue":["Reasignar incidencia"],"NotificationEvent|Reassign merge request":["Reasignar solicitud de fusión"],"NotificationEvent|Reopen issue":["Reabrir incidencia"],"NotificationEvent|Successful pipeline":["Pipeline exitoso"],"NotificationLevel|Custom":["Personalizado"],"NotificationLevel|Disabled":["Deshabilitado"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["Cuando me mencionan"],"NotificationLevel|Participate":["Participación"],"NotificationLevel|Watch":["Vigilancia"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"Project '%{project_name}' queued for deletion.":["Proyecto ‘%{project_name}’ en cola para eliminación."],"Project '%{project_name}' was successfully created.":["Proyecto ‘%{project_name}’ fue creado satisfactoriamente."],"Project '%{project_name}' was successfully updated.":["Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."],"Project '%{project_name}' will be deleted.":["Proyecto ‘%{project_name}’ será eliminado."],"Project access must be granted explicitly to each user.":["El acceso al proyecto debe concederse explícitamente a cada usuario."],"Project export could not be deleted.":["No se pudo eliminar la exportación del proyecto."],"Project export has been deleted.":["La exportación del proyecto ha sido eliminada."],"Project export link has expired. Please generate a new export from your project settings.":["El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."],"Project export started. A download link will be sent by email.":["Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."],"Project home":["Inicio del proyecto"],"ProjectFeature|Disabled":["Deshabilitada"],"ProjectFeature|Everyone with access":["Todos con acceso"],"ProjectFeature|Only team members":["Solo miembros del equipo"],"ProjectFileTree|Name":["Nombre"],"ProjectLastActivity|Never":["Nunca"],"ProjectLifecycle|Stage":["Etapa"],"ProjectNetworkGraph|Graph":["Historial gráfico"],"Read more":["Leer más"],"Readme":["Readme"],"RefSwitcher|Branches":["Ramas"],"RefSwitcher|Tags":["Etiquetas"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Remind later":["Recordar después"],"Remove project":["Eliminar proyecto"],"Request Access":["Solicitar acceso"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Set a password on your account to pull or push via %{protocol}":["Establezca una contraseña en su cuenta para actualizar o enviar a través de% {protocol}"],"Set up CI":["Configurar CI"],"Set up Koding":["Configurar Koding"],"Set up auto deploy":["Configurar auto despliegue"],"SetPasswordToCloneLink|set a password":["establecer una contraseña"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"Source code":["Código fuente"],"StarProject|Star":["Destacar"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The fork relationship has been removed.":["La relación con la bifurcación se ha eliminado."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The project can be accessed by any logged in user.":["El proyecto puede ser accedido por cualquier usuario conectado."],"The project can be accessed without any authentication.":["El proyecto puede accederse sin ninguna autenticación."],"The repository for this project does not exist.":["El repositorio para este proyecto no existe."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Timeago|%s days ago":["hace %s días"],"Timeago|%s days remaining":["%s días restantes"],"Timeago|%s hours remaining":["%s horas restantes"],"Timeago|%s minutes ago":["hace %s minutos"],"Timeago|%s minutes remaining":["%s minutos restantes"],"Timeago|%s months ago":["hace %s meses"],"Timeago|%s months remaining":["%s meses restantes"],"Timeago|%s seconds remaining":["%s segundos restantes"],"Timeago|%s weeks ago":["hace %s semanas"],"Timeago|%s weeks remaining":["%s semanas restantes"],"Timeago|%s years ago":["hace %s años"],"Timeago|%s years remaining":["%s años restantes"],"Timeago|1 day remaining":["1 día restante"],"Timeago|1 hour remaining":["1 hora restante"],"Timeago|1 minute remaining":["1 minuto restante"],"Timeago|1 month remaining":["1 mes restante"],"Timeago|1 week remaining":["1 semana restante"],"Timeago|1 year remaining":["1 año restante"],"Timeago|Past due":["Atrasado"],"Timeago|a day ago":["hace un día"],"Timeago|a month ago":["hace 1 mes"],"Timeago|a week ago":["hace 1 semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace 1 año"],"Timeago|about %s hours ago":["hace alrededor de %s horas"],"Timeago|about a minute ago":["hace alrededor de 1 minuto"],"Timeago|about an hour ago":["hace alrededor de 1 hora"],"Timeago|in %s days":["en %s días"],"Timeago|in %s hours":["en %s horas"],"Timeago|in %s minutes":["en %s minutos"],"Timeago|in %s months":["en %s meses"],"Timeago|in %s seconds":["en %s segundos"],"Timeago|in %s weeks":["en %s semanas"],"Timeago|in %s years":["en %s años"],"Timeago|in 1 day":["en 1 día"],"Timeago|in 1 hour":["en 1 hora"],"Timeago|in 1 minute":["en 1 minuto"],"Timeago|in 1 month":["en 1 mes"],"Timeago|in 1 week":["en 1 semana"],"Timeago|in 1 year":["en 1 año"],"Timeago|less than a minute ago":["hace menos de 1 minuto"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Unstar":["No Destacar"],"Upload New File":["Subir nuevo archivo"],"Upload file":["Subir archivo"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"VisibilityLevel|Internal":["Interno"],"VisibilityLevel|Private":["Privado"],"VisibilityLevel|Public":["Público"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"Withdraw Access Request":["Retirar Solicitud de Acceso"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Va a eliminar %{project_name_with_namespace}.\\n¡El proyecto eliminado NO puede ser restaurado!\\n¿Estás TOTALMENTE seguro?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"],"You can only add files when you are on a branch":["Sólo puede agregar archivos cuando estas en una rama"],"You must sign in to star a project":["Debes iniciar sesión para destacar un proyecto"],"You need permission.":["Necesitas permisos."],"You will not get any notifications via email":["No recibirás ninguna notificación por correo electrónico"],"You will only receive notifications for the events you choose":["Solo recibirás notificaciones de los eventos que elijas"],"You will only receive notifications for threads you have participated in":["Solo recibirás notificaciones de los temas en los que has participado"],"You will receive notifications for any activity":["Recibirás notificaciones para cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones sólo para los comentarios en los que se te mencionó"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"],"Your name":["Tu nombre"],"committed":["cambió"],"day":["día","días"],"notification emails":["correos electrónicos de notificación"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_CN/app.js b/app/assets/javascripts/locale/zh_CN/app.js deleted file mode 100644 index d1335cfbc0f..00000000000 --- a/app/assets/javascripts/locale/zh_CN/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Owner":[""],"Pipeline Health":["流水线健康指标"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["显示 %d 个事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_HK/app.js b/app/assets/javascripts/locale/zh_HK/app.js deleted file mode 100644 index 30cb1e6b89e..00000000000 --- a/app/assets/javascripts/locale/zh_HK/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_HK","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["不可用"],"Not enough data":["數據不足"],"OpenedNDaysAgo|Opened":["開始於"],"Owner":[""],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["項目生命週期"],"Read more":["了解更多"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合並請求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["顯示 %d 個事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合並或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_TW/app.js b/app/assets/javascripts/locale/zh_TW/app.js deleted file mode 100644 index f0fe1e31f18..00000000000 --- a/app/assets/javascripts/locale/zh_TW/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["送交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Interval Pattern":[""],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Owner":[""],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["顯示 %d 個事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index ed7629948ca..d27b4ec78c6 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -299,9 +299,10 @@ $(function () { // Commit show suppressed diff }); $('.navbar-toggle').on('click', function () { - $('.header-content .title').toggle(); + $('.header-content .title, .header-content .navbar-sub-nav').toggle(); $('.header-content .header-logo').toggle(); $('.header-content .navbar-collapse').toggle(); + $('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle(); return $('.navbar-toggle').toggleClass('active'); }); // Show/hide comments on diff diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 894ed81b044..786b6014dc6 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -155,7 +155,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; scrollToElement(container) { if (location.hash) { - const offset = -$('.js-tabs-affix').outerHeight(); + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { offset }); @@ -233,11 +236,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; } mountPipelinesView() { - this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount(); + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + const CommitPipelinesTable = gl.CommitPipelinesTable; + this.commitPipelinesTable = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + // $mount(el) replaces the el with the new rendered component. We need it in order to mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount - document.querySelector('#commit-pipeline-table-view') - .appendChild(this.commitPipelinesTable.$el); + pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); } loadDiff(source) { @@ -284,7 +294,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Scroll any linked note into view // Similar to `toggler_behavior` in the discussion tab const hash = window.gl.utils.getLocationHash(); - const anchor = hash && $container.find(`[id="${hash}"]`); + const anchor = hash && $container.find(`.note[id="${hash}"]`); if (anchor && anchor.length > 0) { const notesContent = anchor.closest('.notes_content'); const lineType = notesContent.hasClass('new') ? 'new' : 'old'; @@ -294,6 +304,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; forceShow: true, }); anchor[0].scrollIntoView(); + window.gl.utils.handleLocationHash(); // We have multiple elements on the page with `#note_xxx` // (discussion and diff tabs) and `:target` only applies to the first anchor.addClass('target'); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 841b24a60a3..3e07ec4d0aa 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -4,83 +4,7 @@ (function() { this.Milestone = (function() { - Milestone.updateIssue = function(li, issue_url, data) { - return $.ajax({ - type: "PUT", - url: issue_url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data, li); - }, - error: function(data) { - return new Flash("Issue update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.sortIssues = function(url, data) { - return $.ajax({ - type: "PUT", - url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data); - }, - error: function() { - return new Flash("Issues update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.sortMergeRequests = function(url, data) { - return $.ajax({ - type: "PUT", - url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data); - }, - error: function(data) { - return new Flash("Issue update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.updateMergeRequest = function(li, merge_request_url, data) { - return $.ajax({ - type: "PUT", - url: merge_request_url, - data: data, - success: function(_data) { - return Milestone.successCallback(_data, li); - }, - error: function(data) { - return new Flash("Issue update failed", 'alert'); - }, - dataType: "json" - }); - }; - - Milestone.successCallback = function(data, element) { - var img_tag; - if (data.assignee) { - img_tag = $('<img/>'); - img_tag.attr('src', data.assignee.avatar_url); - img_tag.addClass('avatar s16'); - $(element).find('.assignee-icon img').replaceWith(img_tag); - } else { - $(element).find('.assignee-icon').empty(); - } - }; - function Milestone() { - this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint'); - this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint'); - - this.bindIssuesSorting(); this.bindTabsSwitching(); // Load merge request tab if it is active @@ -90,22 +14,6 @@ this.loadInitialTab(); } - Milestone.prototype.bindIssuesSorting = function() { - if (!this.issuesSortEndpoint) return; - - $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) { - this.createSortable(el, { - group: 'issue-list', - listEls: $('.issues-sortable-list'), - fieldName: 'issue', - sortCallback: (data) => { - Milestone.sortIssues(this.issuesSortEndpoint, data); - }, - updateCallback: Milestone.updateIssue, - }); - }.bind(this)); - }; - Milestone.prototype.bindTabsSwitching = function() { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { const $target = $(e.target); @@ -115,69 +23,6 @@ }); }; - Milestone.prototype.bindMergeRequestSorting = function() { - if (!this.mergeRequestsSortEndpoint) return; - - $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) { - this.createSortable(el, { - group: 'merge-request-list', - listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"), - fieldName: 'merge_request', - sortCallback: (data) => { - Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data); - }, - updateCallback: Milestone.updateMergeRequest, - }); - }.bind(this)); - }; - - Milestone.prototype.createSortable = function(el, opts) { - return Sortable.create(el, { - group: opts.group, - filter: '.is-disabled', - forceFallback: true, - onStart: function(e) { - opts.listEls.css('min-height', e.item.offsetHeight); - }, - onEnd: function () { - opts.listEls.css("min-height", "0px"); - }, - onUpdate: function(e) { - var ids = this.toArray(), - data; - - if (ids.length) { - data = ids.map(function(id) { - return 'sortable_' + opts.fieldName + '[]=' + id; - }).join('&'); - - opts.sortCallback(data); - } - }, - onAdd: function (e) { - var data, issuableId, issuableUrl, newState; - newState = e.to.dataset.state; - issuableUrl = e.item.dataset.url; - data = (function() { - switch (newState) { - case 'ongoing': - return opts.fieldName + '[assignee_id]=' + gon.current_user_id; - case 'unassigned': - return opts.fieldName + '[assignee_id]='; - case 'closed': - return opts.fieldName + '[state_event]=close'; - } - })(); - if (e.from.dataset.state === 'closed') { - data += '&' + opts.fieldName + '[state_event]=reopen'; - } - - opts.updateCallback(e.item, issuableUrl, data); - this.options.onUpdate.call(this, e); - } - }); - }; - Milestone.prototype.loadInitialTab = function() { const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); @@ -199,10 +44,6 @@ .done((data) => { $(tabElId).html(data.html); $target.addClass('is-loaded'); - - if (tabElId === '#tab-merge-requests') { - this.bindMergeRequestSorting(); - } }); } }; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b0143b12cfe..34476f3303f 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -4,7 +4,7 @@ no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, -newline-per-chained-call, no-useless-escape */ +newline-per-chained-call, no-useless-escape, class-methods-use-this */ /* global Flash */ /* global Autosave */ /* global ResolveService */ @@ -25,1483 +25,1489 @@ import './task_list'; window.autosize = autosize; window.Dropzone = Dropzone; -const normalizeNewlines = function(str) { +function normalizeNewlines(str) { return str.replace(/\r\n/g, '\n'); -}; - -(function() { - this.Notes = (function() { - const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; - const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm; - - Notes.interval = null; - - function Notes(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { - this.updateTargetButtons = this.updateTargetButtons.bind(this); - this.updateComment = this.updateComment.bind(this); - this.visibilityChange = this.visibilityChange.bind(this); - this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); - this.onAddDiffNote = this.onAddDiffNote.bind(this); - this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); - this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); - this.removeNote = this.removeNote.bind(this); - this.cancelEdit = this.cancelEdit.bind(this); - this.updateNote = this.updateNote.bind(this); - this.addDiscussionNote = this.addDiscussionNote.bind(this); - this.addNoteError = this.addNoteError.bind(this); - this.addNote = this.addNote.bind(this); - this.resetMainTargetForm = this.resetMainTargetForm.bind(this); - this.refresh = this.refresh.bind(this); - this.keydownNoteText = this.keydownNoteText.bind(this); - this.toggleCommitList = this.toggleCommitList.bind(this); - this.postComment = this.postComment.bind(this); - this.clearFlashWrapper = this.clearFlash.bind(this); - - this.notes_url = notes_url; - this.note_ids = note_ids; - this.enableGFM = enableGFM; - // Used to keep track of updated notes while people are editing things - this.updatedNotesTrackingMap = {}; - this.last_fetched_at = last_fetched_at; - this.noteable_url = document.URL; - this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); - this.basePollingInterval = 15000; - this.maxPollingSteps = 4; - - this.cleanBinding(); - this.addBinding(); - this.setPollingInterval(); - this.setupMainTargetNoteForm(); - this.taskList = new gl.TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes' - }); - this.collapseLongCommitList(); - this.setViewType(view); - - // We are in the Merge Requests page so we need another edit form for Changes tab - if (gl.utils.getPagePath(1) === 'merge_requests') { - $('.note-edit-form').clone() - .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); - } +} + +const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; +const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; + +export default class Notes { + constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { + this.updateTargetButtons = this.updateTargetButtons.bind(this); + this.updateComment = this.updateComment.bind(this); + this.visibilityChange = this.visibilityChange.bind(this); + this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); + this.onAddDiffNote = this.onAddDiffNote.bind(this); + this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); + this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); + this.removeNote = this.removeNote.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + this.updateNote = this.updateNote.bind(this); + this.addDiscussionNote = this.addDiscussionNote.bind(this); + this.addNoteError = this.addNoteError.bind(this); + this.addNote = this.addNote.bind(this); + this.resetMainTargetForm = this.resetMainTargetForm.bind(this); + this.refresh = this.refresh.bind(this); + this.keydownNoteText = this.keydownNoteText.bind(this); + this.toggleCommitList = this.toggleCommitList.bind(this); + this.postComment = this.postComment.bind(this); + this.clearFlashWrapper = this.clearFlash.bind(this); + this.onHashChange = this.onHashChange.bind(this); + + this.notes_url = notes_url; + this.note_ids = note_ids; + this.enableGFM = enableGFM; + // Used to keep track of updated notes while people are editing things + this.updatedNotesTrackingMap = {}; + this.last_fetched_at = last_fetched_at; + this.noteable_url = document.URL; + this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); + this.basePollingInterval = 15000; + this.maxPollingSteps = 4; + + this.cleanBinding(); + this.addBinding(); + this.setPollingInterval(); + this.setupMainTargetNoteForm(); + this.taskList = new gl.TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes' + }); + this.collapseLongCommitList(); + this.setViewType(view); + + // We are in the Merge Requests page so we need another edit form for Changes tab + if (gl.utils.getPagePath(1) === 'merge_requests') { + $('.note-edit-form').clone() + .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); + } + } + + setViewType(view) { + this.view = Cookies.get('diff_view') || view; + } + + addBinding() { + // Edit note link + $(document).on('click', '.js-note-edit', this.showEditForm.bind(this)); + $(document).on('click', '.note-edit-cancel', this.cancelEdit); + // Reopen and close actions for Issue/MR combined with note form submit + $(document).on('click', '.js-comment-submit-button', this.postComment); + $(document).on('click', '.js-comment-save-button', this.updateComment); + $(document).on('keyup input', '.js-note-text', this.updateTargetButtons); + // resolve a discussion + $(document).on('click', '.js-comment-resolve-button', this.postComment); + // remove a note (in general) + $(document).on('click', '.js-note-delete', this.removeNote); + // delete note attachment + $(document).on('click', '.js-note-attachment-delete', this.removeAttachment); + // reset main target form when clicking discard + $(document).on('click', '.js-note-discard', this.resetMainTargetForm); + // update the file name when an attachment is selected + $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment); + // reply to diff/discussion notes + $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); + // add diff note + $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); + // hide diff note form + $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); + // toggle commit list + $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList); + // fetch notes when tab becomes visible + $(document).on('visibilitychange', this.visibilityChange); + // when issue status changes, we need to refresh data + $(document).on('issuable:change', this.refresh); + // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. + $(document).on('ajax:success', '.js-main-target-form', this.addNote); + $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); + $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); + $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); + // when a key is clicked on the notes + $(document).on('keydown', '.js-note-text', this.keydownNoteText); + // When the URL fragment/hash has changed, `#note_xxx` + return $(window).on('hashchange', this.onHashChange); + } + + cleanBinding() { + $(document).off('click', '.js-note-edit'); + $(document).off('click', '.note-edit-cancel'); + $(document).off('click', '.js-note-delete'); + $(document).off('click', '.js-note-attachment-delete'); + $(document).off('click', '.js-discussion-reply-button'); + $(document).off('click', '.js-add-diff-note-button'); + $(document).off('visibilitychange'); + $(document).off('keyup input', '.js-note-text'); + $(document).off('click', '.js-note-target-reopen'); + $(document).off('click', '.js-note-target-close'); + $(document).off('click', '.js-note-discard'); + $(document).off('keydown', '.js-note-text'); + $(document).off('click', '.js-comment-resolve-button'); + $(document).off('click', '.system-note-commit-list-toggler'); + $(document).off('ajax:success', '.js-main-target-form'); + $(document).off('ajax:success', '.js-discussion-note-form'); + $(document).off('ajax:complete', '.js-main-target-form'); + $(window).off('hashchange', this.onHashChange); + } + + static initCommentTypeToggle(form) { + const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle'); + const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu'); + const noteTypeInput = form.querySelector('#note_type'); + const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'); + const closeButton = form.querySelector('.js-note-target-close'); + const reopenButton = form.querySelector('.js-note-target-reopen'); + + const commentTypeToggle = new CommentTypeToggle({ + dropdownTrigger, + dropdownList, + noteTypeInput, + submitButton, + closeButton, + reopenButton, + }); + + commentTypeToggle.initDroplab(); + } + + keydownNoteText(e) { + var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; + if (gl.utils.isMetaKey(e)) { + return; } - Notes.prototype.setViewType = function(view) { - this.view = Cookies.get('diff_view') || view; - }; - - Notes.prototype.addBinding = function() { - // Edit note link - $(document).on('click', '.js-note-edit', this.showEditForm.bind(this)); - $(document).on('click', '.note-edit-cancel', this.cancelEdit); - // Reopen and close actions for Issue/MR combined with note form submit - $(document).on('click', '.js-comment-submit-button', this.postComment); - $(document).on('click', '.js-comment-save-button', this.updateComment); - $(document).on('keyup input', '.js-note-text', this.updateTargetButtons); - // resolve a discussion - $(document).on('click', '.js-comment-resolve-button', this.postComment); - // remove a note (in general) - $(document).on('click', '.js-note-delete', this.removeNote); - // delete note attachment - $(document).on('click', '.js-note-attachment-delete', this.removeAttachment); - // reset main target form when clicking discard - $(document).on('click', '.js-note-discard', this.resetMainTargetForm); - // update the file name when an attachment is selected - $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment); - // reply to diff/discussion notes - $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); - // add diff note - $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); - // hide diff note form - $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); - // toggle commit list - $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList); - // fetch notes when tab becomes visible - $(document).on('visibilitychange', this.visibilityChange); - // when issue status changes, we need to refresh data - $(document).on('issuable:change', this.refresh); - // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. - $(document).on('ajax:success', '.js-main-target-form', this.addNote); - $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); - $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); - $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); - // when a key is clicked on the notes - return $(document).on('keydown', '.js-note-text', this.keydownNoteText); - }; - - Notes.prototype.cleanBinding = function() { - $(document).off('click', '.js-note-edit'); - $(document).off('click', '.note-edit-cancel'); - $(document).off('click', '.js-note-delete'); - $(document).off('click', '.js-note-attachment-delete'); - $(document).off('click', '.js-discussion-reply-button'); - $(document).off('click', '.js-add-diff-note-button'); - $(document).off('visibilitychange'); - $(document).off('keyup input', '.js-note-text'); - $(document).off('click', '.js-note-target-reopen'); - $(document).off('click', '.js-note-target-close'); - $(document).off('click', '.js-note-discard'); - $(document).off('keydown', '.js-note-text'); - $(document).off('click', '.js-comment-resolve-button'); - $(document).off('click', '.system-note-commit-list-toggler'); - $(document).off('ajax:success', '.js-main-target-form'); - $(document).off('ajax:success', '.js-discussion-note-form'); - $(document).off('ajax:complete', '.js-main-target-form'); - }; - - Notes.initCommentTypeToggle = function (form) { - const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle'); - const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu'); - const noteTypeInput = form.querySelector('#note_type'); - const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'); - const closeButton = form.querySelector('.js-note-target-close'); - const reopenButton = form.querySelector('.js-note-target-reopen'); - - const commentTypeToggle = new CommentTypeToggle({ - dropdownTrigger, - dropdownList, - noteTypeInput, - submitButton, - closeButton, - reopenButton, - }); - - commentTypeToggle.initDroplab(); - }; - - Notes.prototype.keydownNoteText = function(e) { - var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; - if (gl.utils.isMetaKey(e)) { - return; - } - - $textarea = $(e.target); - // Edit previous note when UP arrow is hit - switch (e.which) { - case 38: + $textarea = $(e.target); + // Edit previous note when UP arrow is hit + switch (e.which) { + case 38: + if ($textarea.val() !== '') { + return; + } + myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes')); + if (myLastNote.length) { + myLastNoteEditBtn = myLastNote.find('.js-note-edit'); + return myLastNoteEditBtn.trigger('click', [true, myLastNote]); + } + break; + // Cancel creating diff note or editing any note when ESCAPE is hit + case 27: + discussionNoteForm = $textarea.closest('.js-discussion-note-form'); + if (discussionNoteForm.length) { if ($textarea.val() !== '') { - return; - } - myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, #notes')); - if (myLastNote.length) { - myLastNoteEditBtn = myLastNote.find('.js-note-edit'); - return myLastNoteEditBtn.trigger('click', [true, myLastNote]); - } - break; - // Cancel creating diff note or editing any note when ESCAPE is hit - case 27: - discussionNoteForm = $textarea.closest('.js-discussion-note-form'); - if (discussionNoteForm.length) { - if ($textarea.val() !== '') { - if (!confirm('Are you sure you want to cancel creating this comment?')) { - return; - } + if (!confirm('Are you sure you want to cancel creating this comment?')) { + return; } - this.removeDiscussionNoteForm(discussionNoteForm); - return; } - editNote = $textarea.closest('.note'); - if (editNote.length) { - originalText = $textarea.closest('form').data('original-note'); - newText = $textarea.val(); - if (originalText !== newText) { - if (!confirm('Are you sure you want to cancel editing this comment?')) { - return; - } + this.removeDiscussionNoteForm(discussionNoteForm); + return; + } + editNote = $textarea.closest('.note'); + if (editNote.length) { + originalText = $textarea.closest('form').data('original-note'); + newText = $textarea.val(); + if (originalText !== newText) { + if (!confirm('Are you sure you want to cancel editing this comment?')) { + return; } - return this.removeNoteEditForm(editNote); } - } - }; + return this.removeNoteEditForm(editNote); + } + } + } - Notes.prototype.initRefresh = function() { + initRefresh() { + if (Notes.interval) { clearInterval(Notes.interval); - return Notes.interval = setInterval((function(_this) { - return function() { - return _this.refresh(); - }; - })(this), this.pollingInterval); - }; + } + return Notes.interval = setInterval((function(_this) { + return function() { + return _this.refresh(); + }; + })(this), this.pollingInterval); + } - Notes.prototype.refresh = function() { - if (!document.hidden) { - return this.getContent(); - } - }; + refresh() { + if (!document.hidden) { + return this.getContent(); + } + } - Notes.prototype.getContent = function() { - if (this.refreshing) { - return; - } - this.refreshing = true; - return $.ajax({ - url: this.notes_url, - headers: { 'X-Last-Fetched-At': this.last_fetched_at }, - dataType: 'json', - success: (function(_this) { - return function(data) { - var notes; - notes = data.notes; - _this.last_fetched_at = data.last_fetched_at; - _this.setPollingInterval(data.notes.length); - return $.each(notes, function(i, note) { - _this.renderNote(note); - }); - }; - })(this) - }).always((function(_this) { - return function() { - return _this.refreshing = false; + getContent() { + if (this.refreshing) { + return; + } + this.refreshing = true; + return $.ajax({ + url: this.notes_url, + headers: { 'X-Last-Fetched-At': this.last_fetched_at }, + dataType: 'json', + success: (function(_this) { + return function(data) { + var notes; + notes = data.notes; + _this.last_fetched_at = data.last_fetched_at; + _this.setPollingInterval(data.notes.length); + return $.each(notes, function(i, note) { + _this.renderNote(note); + }); }; - })(this)); - }; - - /* - Increase @pollingInterval up to 120 seconds on every function call, - if `shouldReset` has a truthy value, 'null' or 'undefined' the variable - will reset to @basePollingInterval. - - Note: this function is used to gradually increase the polling interval - if there aren't new notes coming from the server - */ - - Notes.prototype.setPollingInterval = function(shouldReset) { - var nthInterval; - if (shouldReset == null) { - shouldReset = true; - } - nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); - if (shouldReset) { - this.pollingInterval = this.basePollingInterval; - } else if (this.pollingInterval < nthInterval) { - this.pollingInterval *= 2; + })(this) + }).always((function(_this) { + return function() { + return _this.refreshing = false; + }; + })(this)); + } + + /** + * Increase @pollingInterval up to 120 seconds on every function call, + * if `shouldReset` has a truthy value, 'null' or 'undefined' the variable + * will reset to @basePollingInterval. + * + * Note: this function is used to gradually increase the polling interval + * if there aren't new notes coming from the server + */ + setPollingInterval(shouldReset) { + var nthInterval; + if (shouldReset == null) { + shouldReset = true; + } + nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); + if (shouldReset) { + this.pollingInterval = this.basePollingInterval; + } else if (this.pollingInterval < nthInterval) { + this.pollingInterval *= 2; + } + return this.initRefresh(); + } + + handleQuickActions(noteEntity) { + var votesBlock; + if (noteEntity.commands_changes) { + if ('merge' in noteEntity.commands_changes) { + Notes.checkMergeRequestStatus(); } - return this.initRefresh(); - }; - Notes.prototype.handleSlashCommands = function(noteEntity) { - var votesBlock; - if (noteEntity.commands_changes) { - if ('merge' in noteEntity.commands_changes) { - Notes.checkMergeRequestStatus(); - } - - if ('emoji_award' in noteEntity.commands_changes) { - votesBlock = $('.js-awards-block').eq(0); - gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); - return gl.awardsHandler.scrollToAwards(); - } + if ('emoji_award' in noteEntity.commands_changes) { + votesBlock = $('.js-awards-block').eq(0); + gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); + return gl.awardsHandler.scrollToAwards(); } - }; - - Notes.prototype.setupNewNote = function($note) { - // Update datetime format on the recent note - gl.utils.localTimeAgo($note.find('.js-timeago'), false); - this.collapseLongCommitList(); - this.taskList.init(); - }; - - /* - Render note in main comments area. + } + } - Note: for rendering inline notes use renderDiscussionNote - */ + setupNewNote($note) { + // Update datetime format on the recent note + gl.utils.localTimeAgo($note.find('.js-timeago'), false); - Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) { - if (noteEntity.discussion_html) { - return this.renderDiscussionNote(noteEntity, $form); - } + this.collapseLongCommitList(); + this.taskList.init(); - if (!noteEntity.valid) { - if (noteEntity.errors.commands_only) { - this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); - this.refresh(); - } - return; - } + // This stops the note highlight, #note_xxx`, from being removed after real time update + // The `:target` selector does not re-evaluate after we replace element in the DOM + Notes.updateNoteTargetSelector($note); + this.$noteToCleanHighlight = $note; + } - const $note = $notesList.find(`#note_${noteEntity.id}`); - if (Notes.isNewNote(noteEntity, this.note_ids)) { - this.note_ids.push(noteEntity.id); + onHashChange() { + if (this.$noteToCleanHighlight) { + Notes.updateNoteTargetSelector(this.$noteToCleanHighlight); + } - if ($notesList.length) { - $notesList.find('.system-note.being-posted').remove(); - } - const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); + this.$noteToCleanHighlight = null; + } + + static updateNoteTargetSelector($note) { + const hash = gl.utils.getLocationHash(); + // Needs to be an explicit true/false for the jQuery `toggleClass(force)` + const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0); + $note.toggleClass('target', addTargetClass); + } + + /** + * Render note in main comments area. + * + * Note: for rendering inline notes use renderDiscussionNote + */ + renderNote(noteEntity, $form, $notesList = $('.main-notes-list')) { + if (noteEntity.discussion_html) { + return this.renderDiscussionNote(noteEntity, $form); + } - this.setupNewNote($newNote); + if (!noteEntity.valid) { + if (noteEntity.errors.commands_only) { + this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.refresh(); - return this.updateNotesCount(1); } - // The server can send the same update multiple times so we need to make sure to only update once per actual update. - else if (Notes.isUpdatedNote(noteEntity, $note)) { - const isEditing = $note.hasClass('is-editing'); - const initialContent = normalizeNewlines( - $note.find('.original-note-content').text().trim() - ); - const $textarea = $note.find('.js-note-text'); - const currentContent = $textarea.val(); - // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way - const sanitizedNoteNote = normalizeNewlines(noteEntity.note); - const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote; - - if (isEditing && isTextareaUntouched) { - $textarea.val(noteEntity.note); - this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else if (isEditing && !isTextareaUntouched) { - this.putConflictEditWarningInPlace(noteEntity, $note); - this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else { - const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); - this.setupNewNote($updatedNote); - } - } - }; - - Notes.prototype.isParallelView = function() { - return Cookies.get('diff_view') === 'parallel'; - }; - - /* - Render note in discussion area. - - Note: for rendering inline notes use renderDiscussionNote - */ + return; + } - Notes.prototype.renderDiscussionNote = function(noteEntity, $form) { - var discussionContainer, form, row, lineType, diffAvatarContainer; - if (!Notes.isNewNote(noteEntity, this.note_ids)) { - return; - } + const $note = $notesList.find(`#note_${noteEntity.id}`); + if (Notes.isNewNote(noteEntity, this.note_ids)) { this.note_ids.push(noteEntity.id); - form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); - row = form.closest('tr'); - lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; - diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); - // is this the first note of discussion? - discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); - if (!discussionContainer.length) { - discussionContainer = form.closest('.discussion').find('.notes'); - } - if (discussionContainer.length === 0) { - if (noteEntity.diff_discussion_html) { - var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); - - if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { - // insert the note and the reply button after the temp row - row.after($discussion); - } else { - // Merge new discussion HTML in - var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); - var contentContainerClass = '.' + $notes.closest('.notes_content') - .attr('class') - .split(' ') - .join('.'); - - row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); - } - } - // Init discussion on 'Discussion' page if it is merge request page - const page = $('body').attr('data-page'); - if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { - Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); - } - } else { - // append new note to all matching discussions - Notes.animateAppendNote(noteEntity.html, discussionContainer); - } - if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { - gl.diffNotesCompileComponents(); - this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); + if ($notesList.length) { + $notesList.find('.system-note.being-posted').remove(); } + const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); - gl.utils.localTimeAgo($('.js-timeago'), false); - Notes.checkMergeRequestStatus(); + this.setupNewNote($newNote); + this.refresh(); return this.updateNotesCount(1); - }; - - Notes.prototype.getLineHolder = function(changesDiscussionContainer) { - return $(changesDiscussionContainer).closest('.notes_holder') - .prevAll('.line_holder') - .first() - .get(0); - }; - - Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, noteEntity) { - var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); - var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); - - if (!avatarHolder.length) { - avatarHolder = document.createElement('diff-note-avatars'); - avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id); - - diffAvatarContainer.append(avatarHolder); + } + // The server can send the same update multiple times so we need to make sure to only update once per actual update. + else if (Notes.isUpdatedNote(noteEntity, $note)) { + const isEditing = $note.hasClass('is-editing'); + const initialContent = normalizeNewlines( + $note.find('.original-note-content').text().trim() + ); + const $textarea = $note.find('.js-note-text'); + const currentContent = $textarea.val(); + // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way + const sanitizedNoteNote = normalizeNewlines(noteEntity.note); + const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote; - gl.diffNotesCompileComponents(); + if (isEditing && isTextareaUntouched) { + $textarea.val(noteEntity.note); + this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; } - - if (commentButton.length) { - commentButton.remove(); + else if (isEditing && !isTextareaUntouched) { + this.putConflictEditWarningInPlace(noteEntity, $note); + this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; } - }; - - /* - Called in response the main target form has been successfully submitted. - - Removes any errors. - Resets text and preview. - Resets buttons. - */ - - Notes.prototype.resetMainTargetForm = function(e) { - var form; - form = $('.js-main-target-form'); - // remove validation errors - form.find('.js-errors').remove(); - // reset text and preview - form.find('.js-md-write-button').click(); - form.find('.js-note-text').val('').trigger('input'); - form.find('.js-note-text').data('autosave').reset(); - - var event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - form.find('.js-autosize')[0].dispatchEvent(event); - - this.updateTargetButtons(e); - }; - - Notes.prototype.reenableTargetFormSubmitButton = function() { - var form; - form = $('.js-main-target-form'); - return form.find('.js-note-text').trigger('input'); - }; - - /* - Shows the main form and does some setup on it. - - Sets some hidden fields in the form. - */ - - Notes.prototype.setupMainTargetNoteForm = function() { - var form; - // find the form - form = $('.js-new-note-form'); - // Set a global clone of the form for later cloning - this.formClone = form.clone(); - // show the form - this.setupNoteForm(form); - // fix classes - form.removeClass('js-new-note-form'); - form.addClass('js-main-target-form'); - form.find('#note_line_code').remove(); - form.find('#note_position').remove(); - form.find('#note_type').val(''); - form.find('#in_reply_to_discussion_id').remove(); - form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); - this.parentTimeline = form.parents('.timeline'); - - if (form.length) { - Notes.initCommentTypeToggle(form.get(0)); - } - }; - - /* - General note form setup. - - deactivates the submit button when text is empty - hides the preview button when text is empty - setup GFM auto complete - show the form - */ - - Notes.prototype.setupNoteForm = function(form) { - var textarea, key; - new gl.GLForm(form, this.enableGFM); - textarea = form.find('.js-note-text'); - key = [ - 'Note', - form.find('#note_noteable_type').val(), - form.find('#note_noteable_id').val(), - form.find('#note_commit_id').val(), - form.find('#note_type').val(), - form.find('#in_reply_to_discussion_id').val(), - - // LegacyDiffNote - form.find('#note_line_code').val(), - - // DiffNote - form.find('#note_position').val() - ]; - return new Autosave(textarea, key); - }; - - /* - Called in response to the new note form being submitted - - Adds new note to list. - */ - - Notes.prototype.addNote = function($form, note) { - return this.renderNote(note); - }; - - Notes.prototype.addNoteError = function($form) { - let formParentTimeline; - if ($form.hasClass('js-main-target-form')) { - formParentTimeline = $form.parents('.timeline'); - } else if ($form.hasClass('js-discussion-note-form')) { - formParentTimeline = $form.closest('.discussion-notes').find('.notes'); + else { + const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); + this.setupNewNote($updatedNote); } - return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); - }; - - Notes.prototype.updateNoteError = $parentTimeline => new Flash('Your comment could not be updated! Please check your network connection and try again.'); - - /* - Called in response to the new note form being submitted - - Adds new note to list. - */ - - Notes.prototype.addDiscussionNote = function($form, note, isNewDiffComment) { - if ($form.attr('data-resolve-all') != null) { - var projectPath = $form.data('project-path'); - var discussionId = $form.data('discussion-id'); - var mergeRequestId = $form.data('noteable-iid'); + } + } + + isParallelView() { + return Cookies.get('diff_view') === 'parallel'; + } + + /** + * Render note in discussion area. + * + * Note: for rendering inline notes use renderDiscussionNote + */ + renderDiscussionNote(noteEntity, $form) { + var discussionContainer, form, row, lineType, diffAvatarContainer; + if (!Notes.isNewNote(noteEntity, this.note_ids)) { + return; + } + this.note_ids.push(noteEntity.id); + form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); + row = form.closest('tr'); + lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; + diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); + // is this the first note of discussion? + discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); + if (!discussionContainer.length) { + discussionContainer = form.closest('.discussion').find('.notes'); + } + if (discussionContainer.length === 0) { + if (noteEntity.diff_discussion_html) { + var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); - if (ResolveService != null) { - ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId); + if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { + // insert the note and the reply button after the temp row + row.after($discussion); + } else { + // Merge new discussion HTML in + var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); + var contentContainerClass = '.' + $notes.closest('.notes_content') + .attr('class') + .split(' ') + .join('.'); + + row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); } } - - this.renderNote(note, $form); - // cleanup after successfully creating a diff/discussion note - if (isNewDiffComment) { - this.removeDiscussionNoteForm($form); + // Init discussion on 'Discussion' page if it is merge request page + const page = $('body').attr('data-page'); + if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { + Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); } - }; - - /* - Called in response to the edit note form being submitted + } else { + // append new note to all matching discussions + Notes.animateAppendNote(noteEntity.html, discussionContainer); + } - Updates the current note field. - */ + if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { + gl.diffNotesCompileComponents(); + this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); + } - Notes.prototype.updateNote = function(noteEntity, $targetNote) { - var $noteEntityEl, $note_li; - // Convert returned HTML to a jQuery object so we can modify it further - $noteEntityEl = $(noteEntity.html); - $noteEntityEl.addClass('fade-in-full'); - this.revertNoteEditForm($targetNote); - gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl)); - $noteEntityEl.renderGFM(); - $noteEntityEl.find('.js-task-list-container').taskList('enable'); - // Find the note's `li` element by ID and replace it with the updated HTML - $note_li = $('.note-row-' + noteEntity.id); + gl.utils.localTimeAgo($('.js-timeago'), false); + Notes.checkMergeRequestStatus(); + return this.updateNotesCount(1); + } - $note_li.replaceWith($noteEntityEl); + getLineHolder(changesDiscussionContainer) { + return $(changesDiscussionContainer).closest('.notes_holder') + .prevAll('.line_holder') + .first() + .get(0); + } - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - }; + renderDiscussionAvatar(diffAvatarContainer, noteEntity) { + var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); + var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); - Notes.prototype.checkContentToAllowEditing = function($el) { - var initialContent = $el.find('.original-note-content').text().trim(); - var currentContent = $el.find('.js-note-text').val(); - var isAllowed = true; + if (!avatarHolder.length) { + avatarHolder = document.createElement('diff-note-avatars'); + avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id); - if (currentContent === initialContent) { - this.removeNoteEditForm($el); - } - else { - var $buttons = $el.find('.note-form-actions'); - var isWidgetVisible = gl.utils.isInViewport($el.get(0)); + diffAvatarContainer.append(avatarHolder); - if (!isWidgetVisible) { - gl.utils.scrollToElement($el); - } + gl.diffNotesCompileComponents(); + } - $el.find('.js-finish-edit-warning').show(); - isAllowed = false; - } + if (commentButton.length) { + commentButton.remove(); + } + } + + /** + * Called in response the main target form has been successfully submitted. + * + * Removes any errors. + * Resets text and preview. + * Resets buttons. + */ + resetMainTargetForm(e) { + var form; + form = $('.js-main-target-form'); + // remove validation errors + form.find('.js-errors').remove(); + // reset text and preview + form.find('.js-md-write-button').click(); + form.find('.js-note-text').val('').trigger('input'); + form.find('.js-note-text').data('autosave').reset(); + + var event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + form.find('.js-autosize')[0].dispatchEvent(event); + + this.updateTargetButtons(e); + } + + reenableTargetFormSubmitButton() { + var form; + form = $('.js-main-target-form'); + return form.find('.js-note-text').trigger('input'); + } + + /** + * Shows the main form and does some setup on it. + * + * Sets some hidden fields in the form. + */ + setupMainTargetNoteForm() { + var form; + // find the form + form = $('.js-new-note-form'); + // Set a global clone of the form for later cloning + this.formClone = form.clone(); + // show the form + this.setupNoteForm(form); + // fix classes + form.removeClass('js-new-note-form'); + form.addClass('js-main-target-form'); + form.find('#note_line_code').remove(); + form.find('#note_position').remove(); + form.find('#note_type').val(''); + form.find('#in_reply_to_discussion_id').remove(); + form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); + this.parentTimeline = form.parents('.timeline'); + + if (form.length) { + Notes.initCommentTypeToggle(form.get(0)); + } + } + + /** + * General note form setup. + * + * deactivates the submit button when text is empty + * hides the preview button when text is empty + * setup GFM auto complete + * show the form + */ + setupNoteForm(form) { + var textarea, key; + new gl.GLForm(form, this.enableGFM); + textarea = form.find('.js-note-text'); + key = [ + 'Note', + form.find('#note_noteable_type').val(), + form.find('#note_noteable_id').val(), + form.find('#note_commit_id').val(), + form.find('#note_type').val(), + form.find('#in_reply_to_discussion_id').val(), - return isAllowed; - }; + // LegacyDiffNote + form.find('#note_line_code').val(), - /* - Called in response to clicking the edit note link + // DiffNote + form.find('#note_position').val() + ]; + return new Autosave(textarea, key); + } + + /** + * Called in response to the new note form being submitted + * + * Adds new note to list. + */ + addNote($form, note) { + return this.renderNote(note); + } + + addNoteError($form) { + let formParentTimeline; + if ($form.hasClass('js-main-target-form')) { + formParentTimeline = $form.parents('.timeline'); + } else if ($form.hasClass('js-discussion-note-form')) { + formParentTimeline = $form.closest('.discussion-notes').find('.notes'); + } + return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); + } + + updateNoteError($parentTimeline) { + new Flash('Your comment could not be updated! Please check your network connection and try again.'); + } + + /** + * Called in response to the new note form being submitted + * + * Adds new note to list. + */ + addDiscussionNote($form, note, isNewDiffComment) { + if ($form.attr('data-resolve-all') != null) { + var projectPath = $form.data('project-path'); + var discussionId = $form.data('discussion-id'); + var mergeRequestId = $form.data('noteable-iid'); + + if (ResolveService != null) { + ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId); + } + } - Replaces the note text with the note edit form - Adds a data attribute to the form with the original content of the note for cancellations - */ - Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) { - e.preventDefault(); + this.renderNote(note, $form); + // cleanup after successfully creating a diff/discussion note + if (isNewDiffComment) { + this.removeDiscussionNoteForm($form); + } + } + + /** + * Called in response to the edit note form being submitted + * + * Updates the current note field. + */ + updateNote(noteEntity, $targetNote) { + var $noteEntityEl, $note_li; + // Convert returned HTML to a jQuery object so we can modify it further + $noteEntityEl = $(noteEntity.html); + $noteEntityEl.addClass('fade-in-full'); + this.revertNoteEditForm($targetNote); + $noteEntityEl.renderGFM(); + // Find the note's `li` element by ID and replace it with the updated HTML + $note_li = $('.note-row-' + noteEntity.id); + + $note_li.replaceWith($noteEntityEl); + this.setupNewNote($noteEntityEl); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + } - var $target = $(e.target); - var $editForm = $(this.getEditFormSelector($target)); - var $note = $target.closest('.note'); - var $currentlyEditing = $('.note.is-editing:visible'); + checkContentToAllowEditing($el) { + var initialContent = $el.find('.original-note-content').text().trim(); + var currentContent = $el.find('.js-note-text').val(); + var isAllowed = true; - if ($currentlyEditing.length) { - var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); + if (currentContent === initialContent) { + this.removeNoteEditForm($el); + } + else { + var $buttons = $el.find('.note-form-actions'); + var isWidgetVisible = gl.utils.isInViewport($el.get(0)); - if (!isEditAllowed) { - return; - } + if (!isWidgetVisible) { + gl.utils.scrollToElement($el); } - $note.find('.js-note-attachment-delete').show(); - $editForm.addClass('current-note-edit-form'); - $note.addClass('is-editing'); - this.putEditFormInPlace($target); - }; + $el.find('.js-finish-edit-warning').show(); + isAllowed = false; + } - /* - Called in response to clicking the edit note link + return isAllowed; + } - Hides edit form and restores the original note text to the editor textarea. - */ + /** + * Called in response to clicking the edit note link + * + * Replaces the note text with the note edit form + * Adds a data attribute to the form with the original content of the note for cancellations + */ + showEditForm(e, scrollTo, myLastNote) { + e.preventDefault(); - Notes.prototype.cancelEdit = function(e) { - e.preventDefault(); - const $target = $(e.target); - const $note = $target.closest('.note'); - const noteId = $note.attr('data-note-id'); + var $target = $(e.target); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $target.closest('.note'); + var $currentlyEditing = $('.note.is-editing:visible'); - this.revertNoteEditForm($target); + if ($currentlyEditing.length) { + var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); - if (this.updatedNotesTrackingMap[noteId]) { - const $newNote = $(this.updatedNotesTrackingMap[noteId].html); - $note.replaceWith($newNote); - this.setupNewNote($newNote); - // Now that we have taken care of the update, clear it out - delete this.updatedNotesTrackingMap[noteId]; - } - else { - $note.find('.js-finish-edit-warning').hide(); - this.removeNoteEditForm($note); + if (!isEditAllowed) { + return; } - }; - - Notes.prototype.revertNoteEditForm = function($target) { - $target = $target || $('.note.is-editing:visible'); - var selector = this.getEditFormSelector($target); - var $editForm = $(selector); + } - $editForm.insertBefore('.notes-form'); - $editForm.find('.js-comment-save-button').enable(); - $editForm.find('.js-finish-edit-warning').hide(); - }; + $note.find('.js-note-attachment-delete').show(); + $editForm.addClass('current-note-edit-form'); + $note.addClass('is-editing'); + this.putEditFormInPlace($target); + } + + /** + * Called in response to clicking the edit note link + * + * Hides edit form and restores the original note text to the editor textarea. + */ + cancelEdit(e) { + e.preventDefault(); + const $target = $(e.target); + const $note = $target.closest('.note'); + const noteId = $note.attr('data-note-id'); + + this.revertNoteEditForm($target); + + if (this.updatedNotesTrackingMap[noteId]) { + const $newNote = $(this.updatedNotesTrackingMap[noteId].html); + $note.replaceWith($newNote); + this.setupNewNote($newNote); + // Now that we have taken care of the update, clear it out + delete this.updatedNotesTrackingMap[noteId]; + } + else { + $note.find('.js-finish-edit-warning').hide(); + this.removeNoteEditForm($note); + } + } - Notes.prototype.getEditFormSelector = function($el) { - var selector = '.note-edit-form:not(.mr-note-edit-form)'; + revertNoteEditForm($target) { + $target = $target || $('.note.is-editing:visible'); + var selector = this.getEditFormSelector($target); + var $editForm = $(selector); - if ($el.parents('#diffs').length) { - selector = '.note-edit-form.mr-note-edit-form'; - } + $editForm.insertBefore('.notes-form'); + $editForm.find('.js-comment-save-button').enable(); + $editForm.find('.js-finish-edit-warning').hide(); + } - return selector; - }; + getEditFormSelector($el) { + var selector = '.note-edit-form:not(.mr-note-edit-form)'; - Notes.prototype.removeNoteEditForm = function($note) { - var form = $note.find('.current-note-edit-form'); - $note.removeClass('is-editing'); - form.removeClass('current-note-edit-form'); - form.find('.js-finish-edit-warning').hide(); - // Replace markdown textarea text with original note text. - return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); - }; + if ($el.parents('#diffs').length) { + selector = '.note-edit-form.mr-note-edit-form'; + } - /* - Called in response to deleting a note of any kind. - - Removes the actual note from view. - Removes the whole discussion if the last note is being removed. - */ - - Notes.prototype.removeNote = function(e) { - var noteElId, noteId, dataNoteId, $note, lineHolder; - $note = $(e.currentTarget).closest('.note'); - noteElId = $note.attr('id'); - noteId = $note.attr('data-note-id'); - lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') - .closest('.notes_holder') - .prev('.line_holder'); - $(`.note[id="${noteElId}"]`).each((function(_this) { - // A same note appears in the "Discussion" and in the "Changes" tab, we have - // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, - // where $('#noteId') would return only one. - return function(i, el) { - var $note, $notes; - $note = $(el); - $notes = $note.closest('.discussion-notes'); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - if (gl.diffNoteApps[noteElId]) { - gl.diffNoteApps[noteElId].$destroy(); - } + return selector; + } + + removeNoteEditForm($note) { + var form = $note.find('.current-note-edit-form'); + $note.removeClass('is-editing'); + form.removeClass('current-note-edit-form'); + form.find('.js-finish-edit-warning').hide(); + // Replace markdown textarea text with original note text. + return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); + } + + /** + * Called in response to deleting a note of any kind. + * + * Removes the actual note from view. + * Removes the whole discussion if the last note is being removed. + */ + removeNote(e) { + var noteElId, noteId, dataNoteId, $note, lineHolder; + $note = $(e.currentTarget).closest('.note'); + noteElId = $note.attr('id'); + noteId = $note.attr('data-note-id'); + lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') + .closest('.notes_holder') + .prev('.line_holder'); + $(`.note[id="${noteElId}"]`).each((function(_this) { + // A same note appears in the "Discussion" and in the "Changes" tab, we have + // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, + // where $('#noteId') would return only one. + return function(i, el) { + var $note, $notes; + $note = $(el); + $notes = $note.closest('.discussion-notes'); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (gl.diffNoteApps[noteElId]) { + gl.diffNoteApps[noteElId].$destroy(); } + } - $note.remove(); + $note.remove(); - // check if this is the last note for this line - if ($notes.find('.note').length === 0) { - var notesTr = $notes.closest('tr'); + // check if this is the last note for this line + if ($notes.find('.note').length === 0) { + var notesTr = $notes.closest('tr'); - // "Discussions" tab - $notes.closest('.timeline-entry').remove(); + // "Discussions" tab + $notes.closest('.timeline-entry').remove(); - // The notes tr can contain multiple lists of notes, like on the parallel diff - if (notesTr.find('.discussion-notes').length > 1) { - $notes.remove(); - } else { - notesTr.remove(); - } + // The notes tr can contain multiple lists of notes, like on the parallel diff + if (notesTr.find('.discussion-notes').length > 1) { + $notes.remove(); + } else { + notesTr.remove(); } - }; - })(this)); - - Notes.checkMergeRequestStatus(); - return this.updateNotesCount(-1); - }; - - /* - Called in response to clicking the delete attachment link - - Removes the attachment wrapper view, including image tag if it exists - Resets the note editing form - */ + } + }; + })(this)); + + Notes.checkMergeRequestStatus(); + return this.updateNotesCount(-1); + } + + /** + * Called in response to clicking the delete attachment link + * + * Removes the attachment wrapper view, including image tag if it exists + * Resets the note editing form + */ + removeAttachment() { + const $note = $(this).closest('.note'); + $note.find('.note-attachment').remove(); + $note.find('.note-body > .note-text').show(); + $note.find('.note-header').show(); + return $note.find('.current-note-edit-form').remove(); + } + + /** + * Called when clicking on the "reply" button for a diff line. + * + * Shows the note form below the notes. + */ + onReplyToDiscussionNote(e) { + this.replyToDiscussionNote(e.target); + } + + replyToDiscussionNote(target) { + var form, replyLink; + form = this.cleanForm(this.formClone.clone()); + replyLink = $(target).closest('.js-discussion-reply-button'); + // insert the form after the button + replyLink + .closest('.discussion-reply-holder') + .hide() + .after(form); + // show the form + return this.setupDiscussionNoteForm(replyLink, form); + } + + /** + * Shows the diff or discussion form and does some setup on it. + * + * Sets some hidden fields in the form. + * + * Note: dataHolder must have the "discussionId" and "lineCode" data attributes set. + */ + setupDiscussionNoteForm(dataHolder, form) { + // setup note target + var discussionID = dataHolder.data('discussionId'); + + if (discussionID) { + form.attr('data-discussion-id', discussionID); + form.find('#in_reply_to_discussion_id').val(discussionID); + } - Notes.prototype.removeAttachment = function() { - const $note = $(this).closest('.note'); - $note.find('.note-attachment').remove(); - $note.find('.note-body > .note-text').show(); - $note.find('.note-header').show(); - return $note.find('.current-note-edit-form').remove(); - }; + form.attr('data-line-code', dataHolder.data('lineCode')); + form.find('#line_type').val(dataHolder.data('lineType')); - /* - Called when clicking on the "reply" button for a diff line. + form.find('#note_noteable_type').val(dataHolder.data('noteableType')); + form.find('#note_noteable_id').val(dataHolder.data('noteableId')); + form.find('#note_commit_id').val(dataHolder.data('commitId')); + form.find('#note_type').val(dataHolder.data('noteType')); - Shows the note form below the notes. - */ + // LegacyDiffNote + form.find('#note_line_code').val(dataHolder.data('lineCode')); - Notes.prototype.onReplyToDiscussionNote = function(e) { - this.replyToDiscussionNote(e.target); - }; + // DiffNote + form.find('#note_position').val(dataHolder.attr('data-position')); - Notes.prototype.replyToDiscussionNote = function(target) { - var form, replyLink; - form = this.cleanForm(this.formClone.clone()); - replyLink = $(target).closest('.js-discussion-reply-button'); - // insert the form after the button - replyLink - .closest('.discussion-reply-holder') - .hide() - .after(form); - // show the form - return this.setupDiscussionNoteForm(replyLink, form); - }; + form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); + form.find('.js-note-target-close').remove(); + form.find('.js-note-new-discussion').remove(); + this.setupNoteForm(form); - /* - Shows the diff or discussion form and does some setup on it. + form + .removeClass('js-main-target-form') + .addClass('discussion-form js-discussion-note-form'); - Sets some hidden fields in the form. + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + var $commentBtn = form.find('comment-and-resolve-btn'); + $commentBtn.attr(':discussion-id', `'${discussionID}'`); - Note: dataHolder must have the "discussionId" and "lineCode" data attributes set. - */ + gl.diffNotesCompileComponents(); + } - Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { - // setup note target - var discussionID = dataHolder.data('discussionId'); + form.find('.js-note-text').focus(); + form + .find('.js-comment-resolve-button') + .attr('data-discussion-id', discussionID); + } + + /** + * Called when clicking on the "add a comment" button on the side of a diff line. + * + * Inserts a temporary row for the form below the line. + * Sets up the form and shows it. + */ + onAddDiffNote(e) { + e.preventDefault(); + const link = e.currentTarget || e.target; + const $link = $(link); + const showReplyInput = !$link.hasClass('js-diff-comment-avatar'); + this.toggleDiffNote({ + target: $link, + lineType: link.dataset.lineType, + showReplyInput + }); + } + + toggleDiffNote({ + target, + lineType, + forceShow, + showReplyInput = false, + }) { + var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; + $link = $(target); + row = $link.closest('tr'); + const nextRow = row.next(); + let targetRow = row; + if (nextRow.is('.notes_holder')) { + targetRow = nextRow; + } - if (discussionID) { - form.attr('data-discussion-id', discussionID); - form.find('#in_reply_to_discussion_id').val(discussionID); + hasNotes = nextRow.is('.notes_holder'); + addForm = false; + let lineTypeSelector = ''; + rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; + // In parallel view, look inside the correct left/right pane + if (this.isParallelView()) { + lineTypeSelector = `.${lineType}`; + rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; + } + const notesContentSelector = `.notes_content${lineTypeSelector} .content`; + let notesContent = targetRow.find(notesContentSelector); + + if (hasNotes && showReplyInput) { + targetRow.show(); + notesContent = targetRow.find(notesContentSelector); + if (notesContent.length) { + notesContent.show(); + replyButton = notesContent.find('.js-discussion-reply-button:visible'); + if (replyButton.length) { + this.replyToDiscussionNote(replyButton[0]); + } else { + // In parallel view, the form may not be present in one of the panes + noteForm = notesContent.find('.js-discussion-note-form'); + if (noteForm.length === 0) { + addForm = true; + } + } } + } else if (showReplyInput) { + // add a notes row and insert the form + row.after(rowCssToAdd); + targetRow = row.next(); + notesContent = targetRow.find(notesContentSelector); + addForm = true; + } else { + const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible'); + const isForced = forceShow === true || forceShow === false; + const showNow = forceShow === true || (!isCurrentlyShown && !isForced); + + targetRow.toggle(showNow); + notesContent.toggle(showNow); + } - form.attr('data-line-code', dataHolder.data('lineCode')); - form.find('#line_type').val(dataHolder.data('lineType')); - - form.find('#note_noteable_type').val(dataHolder.data('noteableType')); - form.find('#note_noteable_id').val(dataHolder.data('noteableId')); - form.find('#note_commit_id').val(dataHolder.data('commitId')); - form.find('#note_type').val(dataHolder.data('noteType')); - - // LegacyDiffNote - form.find('#note_line_code').val(dataHolder.data('lineCode')); - - // DiffNote - form.find('#note_position').val(dataHolder.attr('data-position')); - - form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); - form.find('.js-note-target-close').remove(); - form.find('.js-note-new-discussion').remove(); - this.setupNoteForm(form); - - form - .removeClass('js-main-target-form') - .addClass('discussion-form js-discussion-note-form'); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - var $commentBtn = form.find('comment-and-resolve-btn'); - $commentBtn.attr(':discussion-id', `'${discussionID}'`); - - gl.diffNotesCompileComponents(); + if (addForm) { + newForm = this.cleanForm(this.formClone.clone()); + newForm.appendTo(notesContent); + // show the form + return this.setupDiscussionNoteForm($link, newForm); + } + } + + /** + * Called in response to "cancel" on a diff note form. + * + * Shows the reply button again. + * Removes the form and if necessary it's temporary row. + */ + removeDiscussionNoteForm(form) { + var glForm, row; + row = form.closest('tr'); + glForm = form.data('gl-form'); + glForm.destroy(); + form.find('.js-note-text').data('autosave').reset(); + // show the reply button (will only work for replies) + form + .prev('.discussion-reply-holder') + .show(); + if (row.is('.js-temp-notes-holder')) { + // remove temporary row for diff lines + return row.remove(); + } else { + // only remove the form + return form.remove(); + } + } + + cancelDiscussionForm(e) { + var form; + e.preventDefault(); + form = $(e.target).closest('.js-discussion-note-form'); + return this.removeDiscussionNoteForm(form); + } + + /** + * Called after an attachment file has been selected. + * + * Updates the file name for the selected attachment. + */ + updateFormAttachment() { + var filename, form; + form = $(this).closest('form'); + // get only the basename + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-attachment-filename').text(filename); + } + + /** + * Called when the tab visibility changes + */ + visibilityChange() { + return this.refresh(); + } + + updateTargetButtons(e) { + var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea; + textarea = $(e.target); + form = textarea.parents('form'); + reopenbtn = form.find('.js-note-target-reopen'); + closebtn = form.find('.js-note-target-close'); + discardbtn = form.find('.js-note-discard'); + + if (textarea.val().trim().length > 0) { + reopentext = reopenbtn.attr('data-alternative-text'); + closetext = closebtn.attr('data-alternative-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); } - - form.find('.js-note-text').focus(); - form - .find('.js-comment-resolve-button') - .attr('data-discussion-id', discussionID); - }; - - /* - Called when clicking on the "add a comment" button on the side of a diff line. - - Inserts a temporary row for the form below the line. - Sets up the form and shows it. - */ - - Notes.prototype.onAddDiffNote = function(e) { - e.preventDefault(); - const link = e.currentTarget || e.target; - const $link = $(link); - const showReplyInput = !$link.hasClass('js-diff-comment-avatar'); - this.toggleDiffNote({ - target: $link, - lineType: link.dataset.lineType, - showReplyInput - }); - }; - - Notes.prototype.toggleDiffNote = function({ - target, - lineType, - forceShow, - showReplyInput = false, - }) { - var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; - $link = $(target); - row = $link.closest('tr'); - const nextRow = row.next(); - let targetRow = row; - if (nextRow.is('.notes_holder')) { - targetRow = nextRow; + if (closebtn.text() !== closetext) { + closebtn.text(closetext); } - - hasNotes = nextRow.is('.notes_holder'); - addForm = false; - let lineTypeSelector = ''; - rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; - // In parallel view, look inside the correct left/right pane - if (this.isParallelView()) { - lineTypeSelector = `.${lineType}`; - rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; + if (reopenbtn.is(':not(.btn-comment-and-reopen)')) { + reopenbtn.addClass('btn-comment-and-reopen'); } - const notesContentSelector = `.notes_content${lineTypeSelector} .content`; - let notesContent = targetRow.find(notesContentSelector); - - if (hasNotes && showReplyInput) { - targetRow.show(); - notesContent = targetRow.find(notesContentSelector); - if (notesContent.length) { - notesContent.show(); - replyButton = notesContent.find('.js-discussion-reply-button:visible'); - if (replyButton.length) { - this.replyToDiscussionNote(replyButton[0]); - } else { - // In parallel view, the form may not be present in one of the panes - noteForm = notesContent.find('.js-discussion-note-form'); - if (noteForm.length === 0) { - addForm = true; - } - } - } - } else if (showReplyInput) { - // add a notes row and insert the form - row.after(rowCssToAdd); - targetRow = row.next(); - notesContent = targetRow.find(notesContentSelector); - addForm = true; - } else { - const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible'); - const isForced = forceShow === true || forceShow === false; - const showNow = forceShow === true || (!isCurrentlyShown && !isForced); - - targetRow.toggle(showNow); - notesContent.toggle(showNow); + if (closebtn.is(':not(.btn-comment-and-close)')) { + closebtn.addClass('btn-comment-and-close'); } - - if (addForm) { - newForm = this.cleanForm(this.formClone.clone()); - newForm.appendTo(notesContent); - // show the form - return this.setupDiscussionNoteForm($link, newForm); + if (discardbtn.is(':hidden')) { + return discardbtn.show(); } - }; - - /* - Called in response to "cancel" on a diff note form. - - Shows the reply button again. - Removes the form and if necessary it's temporary row. - */ - - Notes.prototype.removeDiscussionNoteForm = function(form) { - var glForm, row; - row = form.closest('tr'); - glForm = form.data('gl-form'); - glForm.destroy(); - form.find('.js-note-text').data('autosave').reset(); - // show the reply button (will only work for replies) - form - .prev('.discussion-reply-holder') - .show(); - if (row.is('.js-temp-notes-holder')) { - // remove temporary row for diff lines - return row.remove(); - } else { - // only remove the form - return form.remove(); + } else { + reopentext = reopenbtn.data('original-text'); + closetext = closebtn.data('original-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); } - }; - - Notes.prototype.cancelDiscussionForm = function(e) { - var form; - e.preventDefault(); - form = $(e.target).closest('.js-discussion-note-form'); - return this.removeDiscussionNoteForm(form); - }; - - /* - Called after an attachment file has been selected. - - Updates the file name for the selected attachment. - */ - - Notes.prototype.updateFormAttachment = function() { - var filename, form; - form = $(this).closest('form'); - // get only the basename - filename = $(this).val().replace(/^.*[\\\/]/, ''); - return form.find('.js-attachment-filename').text(filename); - }; - - /* - Called when the tab visibility changes - */ - - Notes.prototype.visibilityChange = function() { - return this.refresh(); - }; - - Notes.prototype.updateTargetButtons = function(e) { - var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea; - textarea = $(e.target); - form = textarea.parents('form'); - reopenbtn = form.find('.js-note-target-reopen'); - closebtn = form.find('.js-note-target-close'); - discardbtn = form.find('.js-note-discard'); - - if (textarea.val().trim().length > 0) { - reopentext = reopenbtn.attr('data-alternative-text'); - closetext = closebtn.attr('data-alternative-text'); - if (reopenbtn.text() !== reopentext) { - reopenbtn.text(reopentext); - } - if (closebtn.text() !== closetext) { - closebtn.text(closetext); - } - if (reopenbtn.is(':not(.btn-comment-and-reopen)')) { - reopenbtn.addClass('btn-comment-and-reopen'); - } - if (closebtn.is(':not(.btn-comment-and-close)')) { - closebtn.addClass('btn-comment-and-close'); - } - if (discardbtn.is(':hidden')) { - return discardbtn.show(); - } - } else { - reopentext = reopenbtn.data('original-text'); - closetext = closebtn.data('original-text'); - if (reopenbtn.text() !== reopentext) { - reopenbtn.text(reopentext); - } - if (closebtn.text() !== closetext) { - closebtn.text(closetext); - } - if (reopenbtn.is('.btn-comment-and-reopen')) { - reopenbtn.removeClass('btn-comment-and-reopen'); - } - if (closebtn.is('.btn-comment-and-close')) { - closebtn.removeClass('btn-comment-and-close'); - } - if (discardbtn.is(':visible')) { - return discardbtn.hide(); - } + if (closebtn.text() !== closetext) { + closebtn.text(closetext); } - }; - - Notes.prototype.putEditFormInPlace = function($el) { - var $editForm = $(this.getEditFormSelector($el)); - var $note = $el.closest('.note'); - - $editForm.insertAfter($note.find('.note-text')); - - var $originalContentEl = $note.find('.original-note-content'); - var originalContent = $originalContentEl.text().trim(); - var postUrl = $originalContentEl.data('post-url'); - var targetId = $originalContentEl.data('target-id'); - var targetType = $originalContentEl.data('target-type'); - - new gl.GLForm($editForm.find('form')); - - $editForm.find('form') - .attr('action', postUrl) - .attr('data-remote', 'true'); - $editForm.find('.js-form-target-id').val(targetId); - $editForm.find('.js-form-target-type').val(targetType); - $editForm.find('.js-note-text').focus().val(originalContent); - $editForm.find('.js-md-write-button').trigger('click'); - $editForm.find('.referenced-users').hide(); - }; - - Notes.prototype.putConflictEditWarningInPlace = function(noteEntity, $note) { - if ($note.find('.js-conflict-edit-warning').length === 0) { - const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> - This comment has changed since you started editing, please review the - <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer"> - updated comment - </a> - to ensure information is not lost - </div>`); - $alert.insertAfter($note.find('.note-text')); + if (reopenbtn.is('.btn-comment-and-reopen')) { + reopenbtn.removeClass('btn-comment-and-reopen'); } - }; - - Notes.prototype.updateNotesCount = function(updateCount) { - return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); - }; - - Notes.prototype.toggleCommitList = function(e) { - const $element = $(e.currentTarget); - const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); - - $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up'); - $closestSystemCommitList.toggleClass('hide-shade'); - }; - - /** - Scans system notes with `ul` elements in system note body - then collapse long commit list pushed by user to make it less - intrusive. - */ - Notes.prototype.collapseLongCommitList = function() { - const systemNotes = $('#notes-list').find('li.system-note').has('ul'); - - $.each(systemNotes, function(index, systemNote) { - const $systemNote = $(systemNote); - const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', ''); - - $systemNote.find('.note-header .system-note-message').html(headerMessage); - - if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) { - $systemNote.find('.note-text').addClass('system-note-commit-list'); - $systemNote.find('.system-note-commit-list-toggler').show(); - } else { - $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade'); - } - }); - }; - - Notes.prototype.addFlash = function(...flashParams) { - this.flashInstance = new Flash(...flashParams); - }; - - Notes.prototype.clearFlash = function() { - if (this.flashInstance && this.flashInstance.flashContainer) { - this.flashInstance.flashContainer.hide(); - this.flashInstance = null; + if (closebtn.is('.btn-comment-and-close')) { + closebtn.removeClass('btn-comment-and-close'); } - }; - - Notes.prototype.cleanForm = function($form) { - // Remove JS classes that are not needed here - $form - .find('.js-comment-type-dropdown') - .removeClass('btn-group'); - - // Remove dropdown - $form - .find('.dropdown-menu') - .remove(); - - return $form; - }; - - /** - * Check if note does not exists on page - */ - Notes.isNewNote = function(noteEntity, noteIds) { - return $.inArray(noteEntity.id, noteIds) === -1; - }; - - /** - * Check if $note already contains the `noteEntity` content - */ - Notes.isUpdatedNote = function(noteEntity, $note) { - // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way - const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); - const currentNoteText = normalizeNewlines( - $note.find('.original-note-content').first().text().trim() - ); - return sanitizedNoteEntityText !== currentNoteText; - }; - - Notes.checkMergeRequestStatus = function() { - if (gl.utils.getPagePath(1) === 'merge_requests') { - gl.mrWidget.checkStatus(); + if (discardbtn.is(':visible')) { + return discardbtn.hide(); } - }; + } + } + + putEditFormInPlace($el) { + var $editForm = $(this.getEditFormSelector($el)); + var $note = $el.closest('.note'); + + $editForm.insertAfter($note.find('.note-text')); + + var $originalContentEl = $note.find('.original-note-content'); + var originalContent = $originalContentEl.text().trim(); + var postUrl = $originalContentEl.data('post-url'); + var targetId = $originalContentEl.data('target-id'); + var targetType = $originalContentEl.data('target-type'); + + new gl.GLForm($editForm.find('form'), this.enableGFM); + + $editForm.find('form') + .attr('action', postUrl) + .attr('data-remote', 'true'); + $editForm.find('.js-form-target-id').val(targetId); + $editForm.find('.js-form-target-type').val(targetType); + $editForm.find('.js-note-text').focus().val(originalContent); + $editForm.find('.js-md-write-button').trigger('click'); + $editForm.find('.referenced-users').hide(); + } + + putConflictEditWarningInPlace(noteEntity, $note) { + if ($note.find('.js-conflict-edit-warning').length === 0) { + const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> + This comment has changed since you started editing, please review the + <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer"> + updated comment + </a> + to ensure information is not lost + </div>`); + $alert.insertAfter($note.find('.note-text')); + } + } - Notes.animateAppendNote = function(noteHtml, $notesList) { - const $note = $(noteHtml); + updateNotesCount(updateCount) { + return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); + } - $note.addClass('fade-in-full').renderGFM(); - $notesList.append($note); - return $note; - }; + toggleCommitList(e) { + const $element = $(e.currentTarget); + const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); - Notes.animateUpdateNote = function(noteHtml, $note) { - const $updatedNote = $(noteHtml); + $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up'); + $closestSystemCommitList.toggleClass('hide-shade'); + } - $updatedNote.addClass('fade-in').renderGFM(); - $note.replaceWith($updatedNote); - return $updatedNote; - }; + /** + * Scans system notes with `ul` elements in system note body + * then collapse long commit list pushed by user to make it less + * intrusive. + */ + collapseLongCommitList() { + const systemNotes = $('#notes-list').find('li.system-note').has('ul'); - /** - * Get data from Form attributes to use for saving/submitting comment. - */ - Notes.prototype.getFormData = function($form) { - return { - formData: $form.serialize(), - formContent: _.escape($form.find('.js-note-text').val()), - formAction: $form.attr('action'), - }; - }; + $.each(systemNotes, function(index, systemNote) { + const $systemNote = $(systemNote); + const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', ''); - /** - * Identify if comment has any slash commands - */ - Notes.prototype.hasSlashCommands = function(formContent) { - return REGEX_SLASH_COMMANDS.test(formContent); - }; + $systemNote.find('.note-header .system-note-message').html(headerMessage); - /** - * Remove slash commands and leave comment with pure message - */ - Notes.prototype.stripSlashCommands = function(formContent) { - return formContent.replace(REGEX_SLASH_COMMANDS, '').trim(); - }; - - /** - * Gets appropriate description from slash commands found in provided `formContent` - */ - Notes.prototype.getSlashCommandDescription = function (formContent, availableSlashCommands = []) { - let tempFormContent; - - // Identify executed slash commands from `formContent` - const executedCommands = availableSlashCommands.filter((command, index) => { - const commandRegex = new RegExp(`/${command.name}`); - return commandRegex.test(formContent); - }); - - if (executedCommands && executedCommands.length) { - if (executedCommands.length > 1) { - tempFormContent = 'Applying multiple commands'; - } else { - const commandDescription = executedCommands[0].description.toLowerCase(); - tempFormContent = `Applying command to ${commandDescription}`; - } + if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) { + $systemNote.find('.note-text').addClass('system-note-commit-list'); + $systemNote.find('.system-note-commit-list-toggler').show(); } else { - tempFormContent = 'Applying command'; + $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade'); } + }); + } - return tempFormContent; - }; + addFlash(...flashParams) { + this.flashInstance = new Flash(...flashParams); + } - /** - * Create placeholder note DOM element populated with comment body - * that we will show while comment is being posted. - * Once comment is _actually_ posted on server, we will have final element - * in response that we will show in place of this temporary element. - */ - Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { - const discussionClass = isDiscussionNote ? 'discussion' : ''; - const $tempNote = $( - `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> - <div class="timeline-entry-inner"> - <div class="timeline-icon"> - <a href="/${currentUsername}"> - <img class="avatar s40" src="${currentUserAvatar}"> - </a> - </div> - <div class="timeline-content ${discussionClass}"> - <div class="note-header"> - <div class="note-header-info"> - <a href="/${currentUsername}"> - <span class="hidden-xs">${currentUserFullname}</span> - <span class="note-headline-light">@${currentUsername}</span> - </a> - </div> - </div> - <div class="note-body"> - <div class="note-text"> - <p>${formContent}</p> - </div> - </div> - </div> - </div> - </li>` - ); - - return $tempNote; + clearFlash() { + if (this.flashInstance && this.flashInstance.flashContainer) { + this.flashInstance.flashContainer.hide(); + this.flashInstance = null; + } + } + + cleanForm($form) { + // Remove JS classes that are not needed here + $form + .find('.js-comment-type-dropdown') + .removeClass('btn-group'); + + // Remove dropdown + $form + .find('.dropdown-menu') + .remove(); + + return $form; + } + + /** + * Check if note does not exists on page + */ + static isNewNote(noteEntity, noteIds) { + return $.inArray(noteEntity.id, noteIds) === -1; + } + + /** + * Check if $note already contains the `noteEntity` content + */ + static isUpdatedNote(noteEntity, $note) { + // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way + const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); + const currentNoteText = normalizeNewlines( + $note.find('.original-note-content').first().text().trim() + ); + return sanitizedNoteEntityText !== currentNoteText; + } + + static checkMergeRequestStatus() { + if (gl.utils.getPagePath(1) === 'merge_requests') { + gl.mrWidget.checkStatus(); + } + } + + static animateAppendNote(noteHtml, $notesList) { + const $note = $(noteHtml); + + $note.addClass('fade-in-full').renderGFM(); + $notesList.append($note); + return $note; + } + + static animateUpdateNote(noteHtml, $note) { + const $updatedNote = $(noteHtml); + + $updatedNote.addClass('fade-in').renderGFM(); + $note.replaceWith($updatedNote); + return $updatedNote; + } + + /** + * Get data from Form attributes to use for saving/submitting comment. + */ + getFormData($form) { + return { + formData: $form.serialize(), + formContent: _.escape($form.find('.js-note-text').val()), + formAction: $form.attr('action'), }; + } + + /** + * Identify if comment has any quick actions + */ + hasQuickActions(formContent) { + return REGEX_QUICK_ACTIONS.test(formContent); + } + + /** + * Remove quick actions and leave comment with pure message + */ + stripQuickActions(formContent) { + return formContent.replace(REGEX_QUICK_ACTIONS, '').trim(); + } + + /** + * Gets appropriate description from quick actions found in provided `formContent` + */ + getQuickActionDescription(formContent, availableQuickActions = []) { + let tempFormContent; + + // Identify executed quick actions from `formContent` + const executedCommands = availableQuickActions.filter((command, index) => { + const commandRegex = new RegExp(`/${command.name}`); + return commandRegex.test(formContent); + }); + + if (executedCommands && executedCommands.length) { + if (executedCommands.length > 1) { + tempFormContent = 'Applying multiple commands'; + } else { + const commandDescription = executedCommands[0].description.toLowerCase(); + tempFormContent = `Applying command to ${commandDescription}`; + } + } else { + tempFormContent = 'Applying command'; + } - /** - * Create Placeholder System Note DOM element populated with slash command description - */ - Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) { - const $tempNote = $( - `<li id="${uniqueId}" class="note system-note timeline-entry being-posted fade-in-half"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <i>${formContent}</i> - </div> + return tempFormContent; + } + + /** + * Create placeholder note DOM element populated with comment body + * that we will show while comment is being posted. + * Once comment is _actually_ posted on server, we will have final element + * in response that we will show in place of this temporary element. + */ + createPlaceholderNote({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { + const discussionClass = isDiscussionNote ? 'discussion' : ''; + const $tempNote = $( + `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> + <div class="timeline-entry-inner"> + <div class="timeline-icon"> + <a href="/${currentUsername}"> + <img class="avatar s40" src="${currentUserAvatar}"> + </a> + </div> + <div class="timeline-content ${discussionClass}"> + <div class="note-header"> + <div class="note-header-info"> + <a href="/${currentUsername}"> + <span class="hidden-xs">${currentUserFullname}</span> + <span class="note-headline-light">@${currentUsername}</span> + </a> + </div> + </div> + <div class="note-body"> + <div class="note-text"> + <p>${formContent}</p> + </div> + </div> + </div> + </div> + </li>` + ); + + return $tempNote; + } + + /** + * Create Placeholder System Note DOM element populated with quick action description + */ + createPlaceholderSystemNote({ formContent, uniqueId }) { + const $tempNote = $( + `<li id="${uniqueId}" class="note system-note timeline-entry being-posted fade-in-half"> + <div class="timeline-entry-inner"> + <div class="timeline-content"> + <i>${formContent}</i> </div> - </li>` - ); + </div> + </li>` + ); + + return $tempNote; + } + + /** + * This method does following tasks step-by-step whenever a new comment + * is submitted by user (both main thread comments as well as discussion comments). + * + * 1) Get Form metadata + * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve + * 3) Build temporary placeholder element (using `createPlaceholderNote`) + * 4) Show placeholder note on UI + * 5) Perform network request to submit the note using `gl.utils.ajaxPost` + * a) If request is successfully completed + * 1. Remove placeholder element + * 2. Show submitted Note element + * 3. Perform post-submit errands + * a. Mark discussion as resolved if comment submission was for resolve. + * b. Reset comment form to original state. + * b) If request failed + * 1. Remove placeholder element + * 2. Show error Flash message about failure + */ + postComment(e) { + e.preventDefault(); + + // Get Form metadata + const $submitBtn = $(e.target); + let $form = $submitBtn.parents('form'); + const $closeBtn = $form.find('.js-note-target-close'); + const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion'; + const isMainForm = $form.hasClass('js-main-target-form'); + const isDiscussionForm = $form.hasClass('js-discussion-note-form'); + const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); + const { formData, formContent, formAction } = this.getFormData($form); + let noteUniqueId; + let systemNoteUniqueId; + let hasQuickActions = false; + let $notesContainer; + let tempFormContent; + + // Get reference to notes container based on type of comment + if (isDiscussionForm) { + $notesContainer = $form.parent('.discussion-notes').find('.notes'); + } else if (isMainForm) { + $notesContainer = $('ul.main-notes-list'); + } - return $tempNote; - }; + // If comment is to resolve discussion, disable submit buttons while + // comment posting is finished. + if (isDiscussionResolve) { + $submitBtn.disable(); + $form.find('.js-comment-submit-button').disable(); + } - /** - * This method does following tasks step-by-step whenever a new comment - * is submitted by user (both main thread comments as well as discussion comments). - * - * 1) Get Form metadata - * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve - * 3) Build temporary placeholder element (using `createPlaceholderNote`) - * 4) Show placeholder note on UI - * 5) Perform network request to submit the note using `gl.utils.ajaxPost` - * a) If request is successfully completed - * 1. Remove placeholder element - * 2. Show submitted Note element - * 3. Perform post-submit errands - * a. Mark discussion as resolved if comment submission was for resolve. - * b. Reset comment form to original state. - * b) If request failed - * 1. Remove placeholder element - * 2. Show error Flash message about failure - */ - Notes.prototype.postComment = function(e) { - e.preventDefault(); - - // Get Form metadata - const $submitBtn = $(e.target); - let $form = $submitBtn.parents('form'); - const $closeBtn = $form.find('.js-note-target-close'); - const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion'; - const isMainForm = $form.hasClass('js-main-target-form'); - const isDiscussionForm = $form.hasClass('js-discussion-note-form'); - const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); - const { formData, formContent, formAction } = this.getFormData($form); - let noteUniqueId; - let systemNoteUniqueId; - let hasSlashCommands = false; - let $notesContainer; - let tempFormContent; - - // Get reference to notes container based on type of comment - if (isDiscussionForm) { - $notesContainer = $form.parent('.discussion-notes').find('.notes'); - } else if (isMainForm) { - $notesContainer = $('ul.main-notes-list'); - } + tempFormContent = formContent; + if (this.hasQuickActions(formContent)) { + tempFormContent = this.stripQuickActions(formContent); + hasQuickActions = true; + } - // If comment is to resolve discussion, disable submit buttons while - // comment posting is finished. - if (isDiscussionResolve) { - $submitBtn.disable(); - $form.find('.js-comment-submit-button').disable(); - } + // Show placeholder note + if (tempFormContent) { + noteUniqueId = _.uniqueId('tempNote_'); + $notesContainer.append(this.createPlaceholderNote({ + formContent: tempFormContent, + uniqueId: noteUniqueId, + isDiscussionNote, + currentUsername: gon.current_username, + currentUserFullname: gon.current_user_fullname, + currentUserAvatar: gon.current_user_avatar_url, + })); + } - tempFormContent = formContent; - if (this.hasSlashCommands(formContent)) { - tempFormContent = this.stripSlashCommands(formContent); - hasSlashCommands = true; - } + // Show placeholder system note + if (hasQuickActions) { + systemNoteUniqueId = _.uniqueId('tempSystemNote_'); + $notesContainer.append(this.createPlaceholderSystemNote({ + formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), + uniqueId: systemNoteUniqueId, + })); + } - // Show placeholder note - if (tempFormContent) { - noteUniqueId = _.uniqueId('tempNote_'); - $notesContainer.append(this.createPlaceholderNote({ - formContent: tempFormContent, - uniqueId: noteUniqueId, - isDiscussionNote, - currentUsername: gon.current_username, - currentUserFullname: gon.current_user_fullname, - currentUserAvatar: gon.current_user_avatar_url, - })); + // Clear the form textarea + if ($notesContainer.length) { + if (isMainForm) { + this.resetMainTargetForm(e); + } else if (isDiscussionForm) { + this.removeDiscussionNoteForm($form); } + } - // Show placeholder system note - if (hasSlashCommands) { - systemNoteUniqueId = _.uniqueId('tempSystemNote_'); - $notesContainer.append(this.createPlaceholderSystemNote({ - formContent: this.getSlashCommandDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), - uniqueId: systemNoteUniqueId, - })); - } + /* eslint-disable promise/catch-or-return */ + // Make request to submit comment on server + gl.utils.ajaxPost(formAction, formData) + .then((note) => { + // Submission successful! remove placeholder + $notesContainer.find(`#${noteUniqueId}`).remove(); - // Clear the form textarea - if ($notesContainer.length) { - if (isMainForm) { - this.resetMainTargetForm(e); - } else if (isDiscussionForm) { - this.removeDiscussionNoteForm($form); + // Reset cached commands list when command is applied + if (hasQuickActions) { + $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); } - } - - /* eslint-disable promise/catch-or-return */ - // Make request to submit comment on server - gl.utils.ajaxPost(formAction, formData) - .then((note) => { - // Submission successful! remove placeholder - $notesContainer.find(`#${noteUniqueId}`).remove(); - // Reset cached commands list when command is applied - if (hasSlashCommands) { - $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); - } + // Clear previous form errors + this.clearFlashWrapper(); - // Clear previous form errors - this.clearFlashWrapper(); + // Check if this was discussion comment + if (isDiscussionForm) { + // Remove flash-container + $notesContainer.find('.flash-container').remove(); - // Check if this was discussion comment - if (isDiscussionForm) { - // Remove flash-container - $notesContainer.find('.flash-container').remove(); - - // If comment intends to resolve discussion, do the same. - if (isDiscussionResolve) { - $form - .attr('data-discussion-id', $submitBtn.data('discussion-id')) - .attr('data-resolve-all', 'true') - .attr('data-project-path', $submitBtn.data('project-path')); - } + // If comment intends to resolve discussion, do the same. + if (isDiscussionResolve) { + $form + .attr('data-discussion-id', $submitBtn.data('discussion-id')) + .attr('data-resolve-all', 'true') + .attr('data-project-path', $submitBtn.data('project-path')); + } - // Show final note element on UI - this.addDiscussionNote($form, note, $notesContainer.length === 0); + // Show final note element on UI + this.addDiscussionNote($form, note, $notesContainer.length === 0); - // append flash-container to the Notes list - if ($notesContainer.length) { - $notesContainer.append('<div class="flash-container" style="display: none;"></div>'); - } - } else if (isMainForm) { // Check if this was main thread comment - // Show final note element on UI and perform form and action buttons cleanup - this.addNote($form, note); - this.reenableTargetFormSubmitButton(e); + // append flash-container to the Notes list + if ($notesContainer.length) { + $notesContainer.append('<div class="flash-container" style="display: none;"></div>'); } + } else if (isMainForm) { // Check if this was main thread comment + // Show final note element on UI and perform form and action buttons cleanup + this.addNote($form, note); + this.reenableTargetFormSubmitButton(e); + } - if (note.commands_changes) { - this.handleSlashCommands(note); - } + if (note.commands_changes) { + this.handleQuickActions(note); + } - $form.trigger('ajax:success', [note]); - }).fail(() => { - // Submission failed, remove placeholder note and show Flash error message - $notesContainer.find(`#${noteUniqueId}`).remove(); + $form.trigger('ajax:success', [note]); + }).fail(() => { + // Submission failed, remove placeholder note and show Flash error message + $notesContainer.find(`#${noteUniqueId}`).remove(); - if (hasSlashCommands) { - $notesContainer.find(`#${systemNoteUniqueId}`).remove(); - } + if (hasQuickActions) { + $notesContainer.find(`#${systemNoteUniqueId}`).remove(); + } - // Show form again on UI on failure - if (isDiscussionForm && $notesContainer.length) { - const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); - this.replyToDiscussionNote(replyButton[0]); - $form = $notesContainer.parent().find('form'); - } + // Show form again on UI on failure + if (isDiscussionForm && $notesContainer.length) { + const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); + this.replyToDiscussionNote(replyButton[0]); + $form = $notesContainer.parent().find('form'); + } - $form.find('.js-note-text').val(formContent); - this.reenableTargetFormSubmitButton(e); - this.addNoteError($form); - }); + $form.find('.js-note-text').val(formContent); + this.reenableTargetFormSubmitButton(e); + this.addNoteError($form); + }); - return $closeBtn.text($closeBtn.data('original-text')); - }; + return $closeBtn.text($closeBtn.data('original-text')); + } + + /** + * This method does following tasks step-by-step whenever an existing comment + * is updated by user (both main thread comments as well as discussion comments). + * + * 1) Get Form metadata + * 2) Update note element with new content + * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost` + * a) If request is successfully completed + * 1. Show submitted Note element + * b) If request failed + * 1. Revert Note element to original content + * 2. Show error Flash message about failure + */ + updateComment(e) { + e.preventDefault(); + + // Get Form metadata + const $submitBtn = $(e.target); + const $form = $submitBtn.parents('form'); + const $closeBtn = $form.find('.js-note-target-close'); + const $editingNote = $form.parents('.note.is-editing'); + const $noteBody = $editingNote.find('.js-task-list-container'); + const $noteBodyText = $noteBody.find('.note-text'); + const { formData, formContent, formAction } = this.getFormData($form); + + // Cache original comment content + const cachedNoteBodyText = $noteBodyText.html(); + + // Show updated comment content temporarily + $noteBodyText.html(formContent); + $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); + $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); + + /* eslint-disable promise/catch-or-return */ + // Make request to update comment on server + gl.utils.ajaxPost(formAction, formData) + .then((note) => { + // Submission successful! render final note element + this.updateNote(note, $editingNote); + }) + .fail(() => { + // Submission failed, revert back to original note + $noteBodyText.html(_.escape(cachedNoteBodyText)); + $editingNote.removeClass('being-posted fade-in'); + $editingNote.find('.fa.fa-spinner').remove(); + + // Show Flash message about failure + this.updateNoteError(); + }); - /** - * This method does following tasks step-by-step whenever an existing comment - * is updated by user (both main thread comments as well as discussion comments). - * - * 1) Get Form metadata - * 2) Update note element with new content - * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost` - * a) If request is successfully completed - * 1. Show submitted Note element - * b) If request failed - * 1. Revert Note element to original content - * 2. Show error Flash message about failure - */ - Notes.prototype.updateComment = function(e) { - e.preventDefault(); - - // Get Form metadata - const $submitBtn = $(e.target); - const $form = $submitBtn.parents('form'); - const $closeBtn = $form.find('.js-note-target-close'); - const $editingNote = $form.parents('.note.is-editing'); - const $noteBody = $editingNote.find('.js-task-list-container'); - const $noteBodyText = $noteBody.find('.note-text'); - const { formData, formContent, formAction } = this.getFormData($form); - - // Cache original comment content - const cachedNoteBodyText = $noteBodyText.html(); - - // Show updated comment content temporarily - $noteBodyText.html(_.escape(formContent)); - $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); - $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); - - /* eslint-disable promise/catch-or-return */ - // Make request to update comment on server - gl.utils.ajaxPost(formAction, formData) - .then((note) => { - // Submission successful! render final note element - this.updateNote(note, $editingNote); - }) - .fail(() => { - // Submission failed, revert back to original note - $noteBodyText.html(_.escape(cachedNoteBodyText)); - $editingNote.removeClass('being-posted fade-in'); - $editingNote.find('.fa.fa-spinner').remove(); - - // Show Flash message about failure - this.updateNoteError(); - }); - - return $closeBtn.text($closeBtn.data('original-text')); - }; + return $closeBtn.text($closeBtn.data('original-text')); + } +} - return Notes; - })(); -}).call(window); +window.Notes = Notes; diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js deleted file mode 100644 index 4d623763ca7..00000000000 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js +++ /dev/null @@ -1,145 +0,0 @@ -import Vue from 'vue'; - -const inputNameAttribute = 'schedule[cron]'; - -export default { - props: { - initialCronInterval: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - inputNameAttribute, - cronInterval: this.initialCronInterval, - cronIntervalPresets: { - everyDay: '0 4 * * *', - everyWeek: '0 4 * * 0', - everyMonth: '0 4 1 * *', - }, - cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', - customInputEnabled: false, - }; - }, - computed: { - intervalIsPreset() { - return _.contains(this.cronIntervalPresets, this.cronInterval); - }, - // The text input is editable when there's a custom interval, or when it's - // a preset interval and the user clicks the 'custom' radio button - isEditable() { - return !!(this.customInputEnabled || !this.intervalIsPreset); - }, - }, - methods: { - toggleCustomInput(shouldEnable) { - this.customInputEnabled = shouldEnable; - - if (shouldEnable) { - // We need to change the value so other radios don't remain selected - // because the model (cronInterval) hasn't changed. The server trims it. - this.cronInterval = `${this.cronInterval} `; - } - }, - }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } - }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - Vue.nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, - }, - template: ` - <div class="interval-pattern-form-group"> - <div class="cron-preset-radio-input"> - <input - id="custom" - class="label-light" - type="radio" - :name="inputNameAttribute" - :value="cronInterval" - :checked="isEditable" - @click="toggleCustomInput(true)" - /> - - <label for="custom"> - Custom - </label> - - <span class="cron-syntax-link-wrap"> - (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>) - </span> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-day" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyDay" - @click="toggleCustomInput(false)" - /> - - <label class="label-light" for="every-day"> - Every day (at 4:00am) - </label> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-week" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyWeek" - @click="toggleCustomInput(false)" - /> - - <label class="label-light" for="every-week"> - Every week (Sundays at 4:00am) - </label> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-month" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyMonth" - @click="toggleCustomInput(false)" - /> - - <label class="label-light" for="every-month"> - Every month (on the 1st at 4:00am) - </label> - </div> - - <div class="cron-interval-input-wrapper"> - <input - id="schedule_cron" - class="form-control inline cron-interval-input" - type="text" - placeholder="Define a custom pattern with cron syntax" - required="true" - v-model="cronInterval" - :name="inputNameAttribute" - :disabled="!isEditable" - /> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue new file mode 100644 index 00000000000..ce46b3fa3fa --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue @@ -0,0 +1,144 @@ +<script> + export default { + props: { + initialCronInterval: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + inputNameAttribute: 'schedule[cron]', + cronInterval: this.initialCronInterval, + cronIntervalPresets: { + everyDay: '0 4 * * *', + everyWeek: '0 4 * * 0', + everyMonth: '0 4 1 * *', + }, + cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', + customInputEnabled: false, + }; + }, + computed: { + intervalIsPreset() { + return _.contains(this.cronIntervalPresets, this.cronInterval); + }, + // The text input is editable when there's a custom interval, or when it's + // a preset interval and the user clicks the 'custom' radio button + isEditable() { + return !!(this.customInputEnabled || !this.intervalIsPreset); + }, + }, + methods: { + toggleCustomInput(shouldEnable) { + this.customInputEnabled = shouldEnable; + + if (shouldEnable) { + // We need to change the value so other radios don't remain selected + // because the model (cronInterval) hasn't changed. The server trims it. + this.cronInterval = `${this.cronInterval} `; + } + }, + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); + }, + }, + }; +</script> + +<template> + <div class="interval-pattern-form-group"> + <div class="cron-preset-radio-input"> + <input + id="custom" + class="label-light" + type="radio" + :name="inputNameAttribute" + :value="cronInterval" + :checked="isEditable" + @click="toggleCustomInput(true)" + /> + + <label for="custom"> + {{ s__('PipelineSheduleIntervalPattern|Custom') }} + </label> + + <span class="cron-syntax-link-wrap"> + (<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) + </span> + </div> + + <div class="cron-preset-radio-input"> + <input + id="every-day" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyDay" + @click="toggleCustomInput(false)" + /> + + <label class="label-light" for="every-day"> + {{ __('Every day (at 4:00am)') }} + </label> + </div> + + <div class="cron-preset-radio-input"> + <input + id="every-week" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyWeek" + @click="toggleCustomInput(false)" + /> + + <label class="label-light" for="every-week"> + {{ __('Every week (Sundays at 4:00am)') }} + </label> + </div> + + <div class="cron-preset-radio-input"> + <input + id="every-month" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyMonth" + @click="toggleCustomInput(false)" + /> + + <label class="label-light" for="every-month"> + {{ __('Every month (on the 1st at 4:00am)') }} + </label> + </div> + + <div class="cron-interval-input-wrapper"> + <input + id="schedule_cron" + class="form-control inline cron-interval-input" + type="text" + :placeholder="__('Define a custom pattern with cron syntax')" + required="true" + v-model="cronInterval" + :name="inputNameAttribute" + :disabled="!isEditable" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js index 5109b110b31..c827b7402dc 100644 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js @@ -1,6 +1,10 @@ +import Vue from 'vue'; import Cookies from 'js-cookie'; +import Translate from '../../vue_shared/translate'; import illustrationSvg from '../icons/intro_illustration.svg'; +Vue.use(Translate); + const cookieKey = 'pipeline_schedules_callout_dismissed'; export default { @@ -29,20 +33,18 @@ export default { </button> <div class="svg-container" v-html="illustrationSvg"></div> <div class="user-callout-copy"> - <h4>Scheduling Pipelines</h4> + <h4>{{ __('Scheduling Pipelines') }}</h4> <p> - The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. - Those scheduled pipelines will inherit limited project access based on their associated user. + {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} </p> - <p> Learn more in the + <p> {{ __('Learn more in the') }} <a :href="docsUrl" target="_blank" - rel="nofollow">pipeline schedules documentation</a>. <!-- oneline to prevent extra space before period --> + rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> </p> </div> </div> </div> `, }; - diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index c60e77decce..b424e7f205d 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -1,20 +1,41 @@ import Vue from 'vue'; -import IntervalPatternInput from './components/interval_pattern_input'; +import Translate from '../vue_shared/translate'; +import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; -document.addEventListener('DOMContentLoaded', () => { - const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput); +Vue.use(Translate); + +function initIntervalPatternInput() { const intervalPatternMount = document.getElementById('interval-pattern-input'); const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : ''; - new IntervalPatternInputComponent({ - propsData: { - initialCronInterval, + return new Vue({ + el: intervalPatternMount, + components: { + intervalPatternInput, }, - }).$mount(intervalPatternMount); + render(createElement) { + return createElement('interval-pattern-input', { + props: { + initialCronInterval, + }, + }); + }, + }); +} + +document.addEventListener('DOMContentLoaded', () => { + /* Most of the form is written in haml, but for fields with more complex behaviors, + * you should mount individual Vue components here. If at some point components need + * to share state, it may make sense to refactor the whole form to Vue */ + + initIntervalPatternInput(); + + // Initialize non-Vue JS components in the form const formElement = document.getElementById('new-pipeline-schedule-form'); + gl.timezoneDropdown = new TimezoneDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 37a6f02d8fd..16cc0761fc1 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,9 +1,9 @@ <script> /* eslint-disable no-new, no-alert */ -/* global Flash */ -import '~/flash'; + import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -11,53 +11,42 @@ export default { type: String, required: true, }, - - service: { - type: Object, - required: true, - }, - title: { type: String, required: true, }, - icon: { type: String, required: true, }, - cssClass: { type: String, required: true, }, - confirmActionMessage: { type: String, required: false, }, }, - + directives: { + tooltip, + }, components: { loadingIcon, }, - data() { return { isLoading: false, }; }, - computed: { iconClass() { return `fa fa-${this.icon}`; }, - buttonClass() { - return `btn has-tooltip ${this.cssClass}`; + return `btn ${this.cssClass}`; }, }, - methods: { onClick() { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { @@ -66,21 +55,10 @@ export default { this.makeRequest(); } }, - makeRequest() { this.isLoading = true; - $(this.$el).tooltip('destroy'); - - this.service.postAction(this.endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); + eventHub.$emit('postAction', this.endpoint); }, }, }; @@ -88,6 +66,7 @@ export default { <template> <button + v-tooltip type="button" @click="onClick" :class="buttonClass" @@ -98,7 +77,8 @@ export default { :disabled="isLoading"> <i :class="iconClass" - aria-hidden="true" /> + aria-hidden="true"> + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 1f9e3d39779..54227425d2a 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,6 +1,6 @@ <script> import getActionIcon from '../../../vue_shared/ci_action_icons'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders either a cancel, retry or play icon pointing to the given path. @@ -29,9 +29,9 @@ }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { actionIconSvg() { @@ -46,12 +46,11 @@ </script> <template> <a + v-tooltip :data-method="actionMethod" :title="tooltipText" :href="link" - ref="tooltip" class="ci-action-icon-container" - data-toggle="tooltip" data-container="body"> <i diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue index 19cafff4e1c..18fe1847eef 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue @@ -1,6 +1,6 @@ <script> import getActionIcon from '../../../vue_shared/ci_action_icons'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders either a cancel, retry or play icon pointing to the given path. @@ -29,9 +29,9 @@ }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { actionIconSvg() { @@ -42,13 +42,12 @@ </script> <template> <a + v-tooltip :data-method="actionMethod" :title="tooltipText" :href="link" - ref="tooltip" rel="nofollow" class="ci-action-icon-wrapper js-ci-status-icon" - data-toggle="tooltip" data-container="body" v-html="actionIconSvg" aria-label="Job's action"> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index d597af8dfb5..2944689a5a7 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,7 +1,7 @@ <script> import jobNameComponent from './job_name_component.vue'; import jobComponent from './job_component.vue'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders the dropdown for the pipeline graph. @@ -34,9 +34,9 @@ }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, components: { jobComponent, @@ -53,12 +53,12 @@ <template> <div> <button + v-tooltip type="button" data-toggle="dropdown" data-container="body" class="dropdown-menu-toggle build-content" - :title="tooltipText" - ref="tooltip"> + :title="tooltipText"> <job-name-component :name="job.name" diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index b39c936101e..1f5ed3f1074 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -2,7 +2,7 @@ import actionComponent from './action_component.vue'; import dropdownActionComponent from './dropdown_action_component.vue'; import jobNameComponent from './job_name_component.vue'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -54,9 +54,9 @@ jobNameComponent, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { tooltipText() { @@ -77,12 +77,11 @@ <template> <div> <a + v-tooltip v-if="job.status.details_path" :href="job.status.details_path" :title="tooltipText" :class="cssClassJobName" - ref="tooltip" - data-toggle="tooltip" data-container="body"> <job-name-component @@ -93,10 +92,9 @@ <div v-else + v-tooltip :title="tooltipText" :class="cssClassJobName" - ref="tooltip" - data-toggle="tooltip" data-container="body"> <job-name-component diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 4781a8ff1da..2ca5ac2912f 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -1,6 +1,6 @@ <script> import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import tooltipMixin from '../../vue_shared/mixins/tooltip'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -12,9 +12,9 @@ export default { components: { userAvatarLink, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { user() { return this.pipeline.user; @@ -23,7 +23,7 @@ export default { }; </script> <template> - <td> + <div class="table-section section-15 hidden-xs hidden-sm"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -42,24 +42,26 @@ export default { class="js-pipeline-url-api api"> API </span> - <span - v-if="pipeline.flags.latest" - class="js-pipeline-url-lastest label label-success" - title="Latest pipeline for this branch" - ref="tooltip"> - latest - </span> - <span - v-if="pipeline.flags.yaml_errors" - class="js-pipeline-url-yaml label label-danger" - :title="pipeline.yaml_errors" - ref="tooltip"> - yaml invalid - </span> - <span - v-if="pipeline.flags.stuck" - class="js-pipeline-url-stuck label label-warning"> - stuck - </span> - </td> + <div class="label-container"> + <span + v-if="pipeline.flags.latest" + v-tooltip + class="js-pipeline-url-latest label label-success" + title="Latest pipeline for this branch"> + latest + </span> + <span + v-if="pipeline.flags.yaml_errors" + v-tooltip + class="js-pipeline-url-yaml label label-danger" + :title="pipeline.yaml_errors"> + yaml invalid + </span> + <span + v-if="pipeline.flags.stuck" + class="js-pipeline-url-stuck label label-warning"> + stuck + </span> + </div> + </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue new file mode 100644 index 00000000000..01ae07aad65 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -0,0 +1,214 @@ +<script> + import PipelinesService from '../services/pipelines_service'; + import pipelinesMixin from '../mixins/pipelines'; + import tablePagination from '../../vue_shared/components/table_pagination.vue'; + import navigationTabs from './navigation_tabs.vue'; + import navigationControls from './nav_controls.vue'; + + export default { + props: { + store: { + type: Object, + required: true, + }, + }, + components: { + tablePagination, + navigationTabs, + navigationControls, + }, + mixins: [ + pipelinesMixin, + ], + data() { + const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; + + return { + endpoint: pipelinesData.endpoint, + cssClass: pipelinesData.cssClass, + helpPagePath: pipelinesData.helpPagePath, + newPipelinePath: pipelinesData.newPipelinePath, + canCreatePipeline: pipelinesData.canCreatePipeline, + allPath: pipelinesData.allPath, + pendingPath: pipelinesData.pendingPath, + runningPath: pipelinesData.runningPath, + finishedPath: pipelinesData.finishedPath, + branchesPath: pipelinesData.branchesPath, + tagsPath: pipelinesData.tagsPath, + hasCi: pipelinesData.hasCi, + ciLintPath: pipelinesData.ciLintPath, + state: this.store.state, + apiScope: 'all', + pagenum: 1, + }; + }, + computed: { + canCreatePipelineParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); + }, + scope() { + const scope = gl.utils.getParameterByName('scope'); + return scope === null ? 'all' : scope; + }, + + /** + * The empty state should only be rendered when the request is made to fetch all pipelines + * and none is returned. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.isLoading && + !this.hasError && + this.hasMadeRequest && + !this.state.pipelines.length && + (this.scope === 'all' || this.scope === null); + }, + /** + * When a specific scope does not have pipelines we render a message. + * + * @return {Boolean} + */ + shouldRenderNoPipelinesMessage() { + return !this.isLoading && + !this.hasError && + !this.state.pipelines.length && + this.scope !== 'all' && + this.scope !== null; + }, + + shouldRenderTable() { + return !this.hasError && + !this.isLoading && this.state.pipelines.length; + }, + /** + * Pagination should only be rendered when there is more than one page. + * + * @return {Boolean} + */ + shouldRenderPagination() { + return !this.isLoading && + this.state.pipelines.length && + this.state.pageInfo.total > this.state.pageInfo.perPage; + }, + hasCiEnabled() { + return this.hasCi !== undefined; + }, + paths() { + return { + allPath: this.allPath, + pendingPath: this.pendingPath, + finishedPath: this.finishedPath, + runningPath: this.runningPath, + branchesPath: this.branchesPath, + tagsPath: this.tagsPath, + }; + }, + pageParameter() { + return gl.utils.getParameterByName('page') || this.pagenum; + }, + scopeParameter() { + return gl.utils.getParameterByName('scope') || this.apiScope; + }, + }, + created() { + this.service = new PipelinesService(this.endpoint); + this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; + }, + methods: { + /** + * Will change the page number and update the URL. + * + * @param {Number} pageNumber desired page to go to. + */ + change(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); + + gl.utils.visitUrl(param); + return param; + }, + + successCallback(resp) { + const response = { + headers: resp.headers, + body: resp.json(), + }; + + this.store.storeCount(response.body.count); + this.store.storePagination(response.headers); + this.setCommonData(response.body.pipelines); + }, + }, + }; +</script> +<template> + <div :class="cssClass"> + <div + class="top-area scrolling-tabs-container inner-page-scroll-tabs" + v-if="!isLoading && !shouldRenderEmptyState"> + <div class="fade-left"> + <i + class="fa fa-angle-left" + aria-hidden="true"> + </i> + </div> + <div class="fade-right"> + <i + class="fa fa-angle-right" + aria-hidden="true"> + </i> + </div> + <navigation-tabs + :scope="scope" + :count="state.count" + :paths="paths" + /> + + <navigation-controls + :new-pipeline-path="newPipelinePath" + :has-ci-enabled="hasCiEnabled" + :help-page-path="helpPagePath" + :ciLintPath="ciLintPath" + :can-create-pipeline="canCreatePipelineParsed " + /> + </div> + + <div class="content-list pipelines"> + + <loading-icon + label="Loading Pipelines" + size="3" + v-if="isLoading" + /> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" + /> + + <error-state v-if="shouldRenderErrorState" /> + + <div + class="blank-state blank-state-no-icon" + v-if="shouldRenderNoPipelinesMessage"> + <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + </div> + + <div + class="table-holder" + v-if="shouldRenderTable"> + + <pipelines-table-component + :pipelines="state.pipelines" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> + + <table-pagination + v-if="shouldRenderPagination" + :change="change" + :pageInfo="state.pageInfo" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.js b/app/assets/javascripts/pipelines/components/pipelines_actions.js deleted file mode 100644 index b9e066c5db1..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.js +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-disable no-new */ -/* global Flash */ -import '~/flash'; -import playIconSvg from 'icons/_icon_play.svg'; -import eventHub from '../event_hub'; -import loadingIconComponent from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - actions: { - type: Array, - required: true, - }, - - service: { - type: Object, - required: true, - }, - }, - - components: { - loadingIconComponent, - }, - - data() { - return { - playIconSvg, - isLoading: false, - }; - }, - - methods: { - onClickAction(endpoint) { - this.isLoading = true; - - $(this.$refs.tooltip).tooltip('destroy'); - - this.service.postAction(endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); - }, - - isActionDisabled(action) { - if (action.playable === undefined) { - return false; - } - - return !action.playable; - }, - }, - - template: ` - <div class="btn-group" v-if="actions"> - <button - type="button" - class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" - title="Manual job" - data-toggle="dropdown" - data-placement="top" - aria-label="Manual job" - ref="tooltip" - :disabled="isLoading"> - ${playIconSvg} - <i - class="fa fa-caret-down" - aria-hidden="true" /> - <loading-icon v-if="isLoading" /> - </button> - - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> - <button - type="button" - class="js-pipeline-action-link no-btn btn" - @click="onClickAction(action.path)" - :class="{ 'disabled': isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> - ${playIconSvg} - <span>{{action.name}}</span> - </button> - </li> - </ul> - </div> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue new file mode 100644 index 00000000000..01dfe51cc17 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -0,0 +1,78 @@ +<script> + /* global Flash */ + import '~/flash'; + import playIconSvg from 'icons/_icon_play.svg'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + props: { + actions: { + type: Array, + required: true, + }, + }, + directives: { + tooltip, + }, + components: { + loadingIcon, + }, + data() { + return { + playIconSvg, + isLoading: false, + }; + }, + methods: { + onClickAction(endpoint) { + this.isLoading = true; + + eventHub.$emit('postAction', endpoint); + }, + + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } + + return !action.playable; + }, + }, + }; +</script> +<template> + <div class="btn-group"> + <button + v-tooltip + type="button" + class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" + title="Manual job" + data-toggle="dropdown" + data-placement="top" + aria-label="Manual job" + :disabled="isLoading"> + <span v-html="playIconSvg"></span> + <i + class="fa fa-caret-down" + aria-hidden="true"> + </i> + <loading-icon v-if="isLoading" /> + </button> + + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="action in actions"> + <button + type="button" + class="js-pipeline-action-link no-btn btn" + @click="onClickAction(action.path)" + :class="{ disabled: isActionDisabled(action) }" + :disabled="isActionDisabled(action)"> + <span v-html="playIconSvg"></span> + <span>{{action.name}}</span> + </button> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.js b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js deleted file mode 100644 index f18e2dfadaf..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.js +++ /dev/null @@ -1,33 +0,0 @@ -export default { - props: { - artifacts: { - type: Array, - required: true, - }, - }, - - template: ` - <div class="btn-group" role="group"> - <button - class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" - title="Artifacts" - data-placement="top" - data-toggle="dropdown" - aria-label="Artifacts"> - <i class="fa fa-download" aria-hidden="true"></i> - <i class="fa fa-caret-down" aria-hidden="true"></i> - </button> - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="artifact in artifacts"> - <a - rel="nofollow" - download - :href="artifact.path"> - <i class="fa fa-download" aria-hidden="true"></i> - <span>Download {{artifact.name}} artifacts</span> - </a> - </li> - </ul> - </div> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue new file mode 100644 index 00000000000..b19bd509a00 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -0,0 +1,51 @@ +<script> + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + props: { + artifacts: { + type: Array, + required: true, + }, + }, + directives: { + tooltip, + }, + }; +</script> +<template> + <div + class="btn-group" + role="group"> + <button + v-tooltip + class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" + title="Artifacts" + data-placement="top" + data-toggle="dropdown" + aria-label="Artifacts"> + <i + class="fa fa-download" + aria-hidden="true"> + </i> + <i + class="fa fa-caret-down" + aria-hidden="true"> + </i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="artifact in artifacts"> + <a + rel="nofollow" + download + :href="artifact.path"> + <i + class="fa fa-download" + aria-hidden="true"> + </i> + <span>Download {{artifact.name}} artifacts</span> + </a> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue new file mode 100644 index 00000000000..5088d92209f --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -0,0 +1,59 @@ +<script> + import pipelinesTableRowComponent from './pipelines_table_row.vue'; + + /** + * Pipelines Table Component. + * + * Given an array of objects, renders a table. + */ + export default { + props: { + pipelines: { + type: Array, + required: true, + }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, + }, + }, + components: { + pipelinesTableRowComponent, + }, + }; +</script> +<template> + <div class="ci-table"> + <div + class="gl-responsive-table-row table-row-header" + role="row"> + <div + class="table-section section-10 js-pipeline-status pipeline-status" + role="rowheader"> + Status + </div> + <div + class="table-section section-15 js-pipeline-info pipeline-info" + role="rowheader"> + Pipeline + </div> + <div + class="table-section section-25 js-pipeline-commit pipeline-commit" + role="rowheader"> + Commit + </div> + <div + class="table-section section-15 js-pipeline-stages pipeline-stages" + role="rowheader"> + Stages + </div> + </div> + <pipelines-table-row-component + v-for="model in pipelines" + :key="model.id" + :pipeline="model" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index f60f8eeb43d..c3f1c426d8a 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,12 +1,13 @@ +<script> /* eslint-disable no-param-reassign */ -import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; -import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; -import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; -import ciBadge from './ci_badge_link.vue'; -import PipelinesStageComponent from '../../pipelines/components/stage.vue'; -import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue'; -import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; -import CommitComponent from './commit'; +import asyncButtonComponent from './async_button.vue'; +import pipelinesActionsComponent from './pipelines_actions.vue'; +import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; +import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; +import pipelineStage from './stage.vue'; +import pipelineUrl from './pipeline_url.vue'; +import pipelinesTimeago from './time_ago.vue'; +import commitComponent from '../../vue_shared/components/commit.vue'; /** * Pipeline table row. @@ -19,30 +20,22 @@ export default { type: Object, required: true, }, - - service: { - type: Object, - required: true, - }, - updateGraphDropdown: { type: Boolean, required: false, default: false, }, }, - components: { - 'async-button-component': AsyncButtonComponent, - 'pipelines-actions-component': PipelinesActionsComponent, - 'pipelines-artifacts-component': PipelinesArtifactsComponent, - 'commit-component': CommitComponent, - 'dropdown-stage': PipelinesStageComponent, - 'pipeline-url': PipelinesUrlComponent, + asyncButtonComponent, + pipelinesActionsComponent, + pipelinesArtifactsComponent, + commitComponent, + pipelineStage, + pipelineUrl, ciBadge, - 'time-ago': PipelinesTimeagoComponent, + pipelinesTimeago, }, - computed: { /** * If provided, returns the commit tag. @@ -203,17 +196,37 @@ export default { } return {}; }, - }, - template: ` - <tr class="commit"> - <td class="commit-link"> + displayPipelineActions() { + return this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length; + }, + }, +}; +</script> +<template> + <div class="commit gl-responsive-table-row"> + <div class="table-section section-10 commit-link"> + <div class="table-mobile-header" + role="rowheader"> + Status + </div> + <div class="table-mobile-content"> <ci-badge :status="pipelineStatus"/> - </td> + </div> + </div> - <pipeline-url :pipeline="pipeline"></pipeline-url> + <pipeline-url :pipeline="pipeline" /> - <td> + <div class="table-section section-25"> + <div + class="table-mobile-header" + role="rowheader"> + Commit + </div> + <div class="table-mobile-content"> <commit-component :tag="commitTag" :commit-ref="commitRef" @@ -221,52 +234,64 @@ export default { :short-sha="commitShortSha" :title="commitTitle" :author="commitAuthor"/> - </td> + </div> + </div> - <td class="stage-cell"> + <div class="table-section section-wrap section-15 stage-cell"> + <div + class="table-mobile-header" + role="rowheader"> + Stages + </div> + <div class="table-mobile-content"> <div class="stage-container dropdown js-mini-pipeline-graph" v-if="pipeline.details.stages.length > 0" v-for="stage in pipeline.details.stages"> - - <dropdown-stage + <pipeline-stage :stage="stage" - :update-dropdown="updateGraphDropdown"/> + :update-dropdown="updateGraphDropdown" + /> </div> - </td> + </div> + </div> - <time-ago - :duration="pipelineDuration" - :finished-time="pipelineFinishedAt" /> + <pipelines-timeago + :duration="pipelineDuration" + :finished-time="pipelineFinishedAt" + /> - <td class="pipeline-actions"> - <div class="pull-right btn-group"> - <pipelines-actions-component - v-if="pipeline.details.manual_actions.length" - :actions="pipeline.details.manual_actions" - :service="service" /> + <div + v-if="displayPipelineActions" + class="table-section section-20 table-button-footer pipeline-actions"> + <div class="btn-group table-action-buttons"> + <pipelines-actions-component + v-if="pipeline.details.manual_actions.length" + :actions="pipeline.details.manual_actions" + /> - <pipelines-artifacts-component - v-if="pipeline.details.artifacts.length" - :artifacts="pipeline.details.artifacts" /> + <pipelines-artifacts-component + v-if="pipeline.details.artifacts.length" + class="hidden-xs hidden-sm" + :artifacts="pipeline.details.artifacts" + /> - <async-button-component - v-if="pipeline.flags.retryable" - :service="service" - :endpoint="pipeline.retry_path" - css-class="js-pipelines-retry-button btn-default btn-retry" - title="Retry" - icon="repeat" /> + <async-button-component + v-if="pipeline.flags.retryable" + :endpoint="pipeline.retry_path" + css-class="js-pipelines-retry-button btn-default btn-retry" + title="Retry" + icon="repeat" + /> - <async-button-component - v-if="pipeline.flags.cancelable" - :service="service" - :endpoint="pipeline.cancel_path" - css-class="js-pipelines-cancel-button btn-remove" - title="Cancel" - icon="remove" - confirm-action-message="Are you sure you want to cancel this pipeline?" /> - </div> - </td> - </tr> - `, -}; + <async-button-component + v-if="pipeline.flags.cancelable" + :endpoint="pipeline.cancel_path" + css-class="js-pipelines-cancel-button btn-remove" + title="Cancel" + icon="remove" + confirm-action-message="Are you sure you want to cancel this pipeline?" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index c05c76c9a64..87b2725a045 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -16,7 +16,7 @@ /* global Flash */ import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltipMixin from '../../vue_shared/mixins/tooltip'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -32,15 +32,14 @@ export default { }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, data() { return { isLoading: false, dropdownContent: '', - endpoint: this.stage.dropdown_path, }; }, @@ -73,7 +72,7 @@ export default { }, fetchJobs() { - this.$http.get(this.endpoint) + this.$http.get(this.stage.dropdown_path) .then((response) => { this.dropdownContent = response.json().html; this.isLoading = false; @@ -132,7 +131,7 @@ export default { <template> <div class="dropdown"> <button - ref="tooltip" + v-tooltip :class="triggerButtonClass" @click="onClickStage" class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" diff --git a/app/assets/javascripts/pipelines/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js deleted file mode 100644 index 188f74cc705..00000000000 --- a/app/assets/javascripts/pipelines/components/time_ago.js +++ /dev/null @@ -1,98 +0,0 @@ -import iconTimerSvg from 'icons/_icon_timer.svg'; -import '../../lib/utils/datetime_utility'; - -export default { - props: { - finishedTime: { - type: String, - required: true, - }, - - duration: { - type: Number, - required: true, - }, - }, - - data() { - return { - iconTimerSvg, - }; - }, - - updated() { - $(this.$refs.tooltip).tooltip('fixTitle'); - }, - - computed: { - hasDuration() { - return this.duration > 0; - }, - - hasFinishedTime() { - return this.finishedTime !== ''; - }, - - localTimeFinished() { - return gl.utils.formatDate(this.finishedTime); - }, - - durationFormated() { - const date = new Date(this.duration * 1000); - - let hh = date.getUTCHours(); - let mm = date.getUTCMinutes(); - let ss = date.getSeconds(); - - // left pad - if (hh < 10) { - hh = `0${hh}`; - } - if (mm < 10) { - mm = `0${mm}`; - } - if (ss < 10) { - ss = `0${ss}`; - } - - return `${hh}:${mm}:${ss}`; - }, - - finishedTimeFormated() { - const timeAgo = gl.utils.getTimeago(); - - return timeAgo.format(this.finishedTime); - }, - }, - - template: ` - <td class="pipelines-time-ago"> - <p - class="duration" - v-if="hasDuration"> - <span - v-html="iconTimerSvg"> - </span> - {{durationFormated}} - </p> - - <p - class="finished-at" - v-if="hasFinishedTime"> - - <i - class="fa fa-calendar" - aria-hidden="true" /> - - <time - ref="tooltip" - data-toggle="tooltip" - data-placement="top" - data-container="body" - :title="localTimeFinished"> - {{finishedTimeFormated}} - </time> - </p> - </td> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue new file mode 100644 index 00000000000..037684b4e72 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -0,0 +1,95 @@ +<script> + import iconTimerSvg from 'icons/_icon_timer.svg'; + import '../../lib/utils/datetime_utility'; + import tooltip from '../../vue_shared/directives/tooltip'; + import timeagoMixin from '../../vue_shared/mixins/timeago'; + + export default { + props: { + finishedTime: { + type: String, + required: true, + }, + duration: { + type: Number, + required: true, + }, + }, + mixins: [ + timeagoMixin, + ], + directives: { + tooltip, + }, + data() { + return { + iconTimerSvg, + }; + }, + computed: { + hasDuration() { + return this.duration > 0; + }, + hasFinishedTime() { + return this.finishedTime !== ''; + }, + durationFormated() { + const date = new Date(this.duration * 1000); + + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); + + // left pad + if (hh < 10) { + hh = `0${hh}`; + } + if (mm < 10) { + mm = `0${mm}`; + } + if (ss < 10) { + ss = `0${ss}`; + } + + return `${hh}:${mm}:${ss}`; + }, + }, + }; +</script> +<template> + <div class="table-section section-15 pipelines-time-ago"> + <div + class="table-mobile-header" + role="rowheader"> + Duration + </div> + <div class="table-mobile-content"> + <p + class="duration" + v-if="hasDuration"> + <span + v-html="iconTimerSvg"> + </span> + {{durationFormated}} + </p> + + <p + class="finished-at hidden-xs hidden-sm" + v-if="hasFinishedTime"> + + <i + class="fa fa-calendar" + aria-hidden="true"> + </i> + + <time + v-tooltip + data-placement="top" + data-container="body" + :title="tooltipTitle(finishedTime)"> + {{timeFormated(finishedTime)}} + </time> + </p> + </div> + </div> +</script> diff --git a/app/assets/javascripts/pipelines/index.js b/app/assets/javascripts/pipelines/index.js deleted file mode 100644 index 48f9181a8d9..00000000000 --- a/app/assets/javascripts/pipelines/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import Vue from 'vue'; -import PipelinesStore from './stores/pipelines_store'; -import PipelinesComponent from './pipelines'; -import '../vue_shared/vue_resource_interceptor'; - -$(() => new Vue({ - el: document.querySelector('#pipelines-list-vue'), - - data() { - const store = new PipelinesStore(); - - return { - store, - }; - }, - components: { - 'vue-pipelines': PipelinesComponent, - }, - template: ` - <vue-pipelines :store="store" /> - `, -})); diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js new file mode 100644 index 00000000000..9adc15e6266 --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -0,0 +1,103 @@ +/* global Flash */ +import '~/flash'; +import Visibility from 'visibilityjs'; +import Poll from '../../lib/utils/poll'; +import emptyState from '../components/empty_state.vue'; +import errorState from '../components/error_state.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import pipelinesTableComponent from '../components/pipelines_table.vue'; +import eventHub from '../event_hub'; + +export default { + components: { + pipelinesTableComponent, + errorState, + emptyState, + loadingIcon, + }, + computed: { + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + }, + data() { + return { + isLoading: false, + hasError: false, + isMakingRequest: false, + updateGraphDropdown: false, + hasMadeRequest: false, + }; + }, + beforeMount() { + this.poll = new Poll({ + resource: this.service, + method: 'getPipelines', + data: this.requestData ? this.requestData : undefined, + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + eventHub.$on('postAction', this.postAction); + }, + beforeDestroy() { + eventHub.$off('refreshPipelines'); + eventHub.$on('postAction', this.postAction); + }, + destroyed() { + this.poll.stop(); + }, + methods: { + fetchPipelines() { + if (!this.isMakingRequest) { + this.isLoading = true; + + this.service.getPipelines(this.requestData) + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + }, + setCommonData(pipelines) { + this.store.storePipelines(pipelines); + this.isLoading = false; + this.updateGraphDropdown = true; + this.hasMadeRequest = true; + }, + errorCallback() { + this.hasError = true; + this.isLoading = false; + this.updateGraphDropdown = false; + }, + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; + + if (isMakingRequest) { + this.updateGraphDropdown = false; + } + }, + postAction(endpoint) { + this.service.postAction(endpoint) + .then(() => eventHub.$emit('refreshPipelines')) + .catch(() => new Flash('An error occured while making the request.')); + }, + }, +}; diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js deleted file mode 100644 index b530461837c..00000000000 --- a/app/assets/javascripts/pipelines/pipelines.js +++ /dev/null @@ -1,293 +0,0 @@ -import Visibility from 'visibilityjs'; -import PipelinesService from './services/pipelines_service'; -import eventHub from './event_hub'; -import pipelinesTableComponent from '../vue_shared/components/pipelines_table'; -import tablePagination from '../vue_shared/components/table_pagination.vue'; -import emptyState from './components/empty_state.vue'; -import errorState from './components/error_state.vue'; -import navigationTabs from './components/navigation_tabs.vue'; -import navigationControls from './components/nav_controls.vue'; -import loadingIcon from '../vue_shared/components/loading_icon.vue'; -import Poll from '../lib/utils/poll'; - -export default { - props: { - store: { - type: Object, - required: true, - }, - }, - - components: { - tablePagination, - pipelinesTableComponent, - emptyState, - errorState, - navigationTabs, - navigationControls, - loadingIcon, - }, - - data() { - const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; - - return { - endpoint: pipelinesData.endpoint, - cssClass: pipelinesData.cssClass, - helpPagePath: pipelinesData.helpPagePath, - newPipelinePath: pipelinesData.newPipelinePath, - canCreatePipeline: pipelinesData.canCreatePipeline, - allPath: pipelinesData.allPath, - pendingPath: pipelinesData.pendingPath, - runningPath: pipelinesData.runningPath, - finishedPath: pipelinesData.finishedPath, - branchesPath: pipelinesData.branchesPath, - tagsPath: pipelinesData.tagsPath, - hasCi: pipelinesData.hasCi, - ciLintPath: pipelinesData.ciLintPath, - state: this.store.state, - apiScope: 'all', - pagenum: 1, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, - }; - }, - - computed: { - canCreatePipelineParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); - }, - - scope() { - const scope = gl.utils.getParameterByName('scope'); - return scope === null ? 'all' : scope; - }, - - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, - - /** - * The empty state should only be rendered when the request is made to fetch all pipelines - * and none is returned. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.isLoading && - !this.hasError && - this.hasMadeRequest && - !this.state.pipelines.length && - (this.scope === 'all' || this.scope === null); - }, - - /** - * When a specific scope does not have pipelines we render a message. - * - * @return {Boolean} - */ - shouldRenderNoPipelinesMessage() { - return !this.isLoading && - !this.hasError && - !this.state.pipelines.length && - this.scope !== 'all' && - this.scope !== null; - }, - - shouldRenderTable() { - return !this.hasError && - !this.isLoading && this.state.pipelines.length; - }, - - /** - * Pagination should only be rendered when there is more than one page. - * - * @return {Boolean} - */ - shouldRenderPagination() { - return !this.isLoading && - this.state.pipelines.length && - this.state.pageInfo.total > this.state.pageInfo.perPage; - }, - - hasCiEnabled() { - return this.hasCi !== undefined; - }, - - paths() { - return { - allPath: this.allPath, - pendingPath: this.pendingPath, - finishedPath: this.finishedPath, - runningPath: this.runningPath, - branchesPath: this.branchesPath, - tagsPath: this.tagsPath, - }; - }, - - pageParameter() { - return gl.utils.getParameterByName('page') || this.pagenum; - }, - - scopeParameter() { - return gl.utils.getParameterByName('scope') || this.apiScope; - }, - }, - - created() { - this.service = new PipelinesService(this.endpoint); - - const poll = new Poll({ - resource: this.service, - method: 'getPipelines', - data: { page: this.pageParameter, scope: this.scopeParameter }, - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - poll.restart(); - } else { - poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - - beforeDestroy() { - eventHub.$off('refreshPipelines'); - }, - - methods: { - /** - * Will change the page number and update the URL. - * - * @param {Number} pageNumber desired page to go to. - */ - change(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); - - gl.utils.visitUrl(param); - return param; - }, - - fetchPipelines() { - if (!this.isMakingRequest) { - this.isLoading = true; - - this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - } - }, - - successCallback(resp) { - const response = { - headers: resp.headers, - body: resp.json(), - }; - - this.store.storeCount(response.body.count); - this.store.storePipelines(response.body.pipelines); - this.store.storePagination(response.headers); - - this.isLoading = false; - this.updateGraphDropdown = true; - this.hasMadeRequest = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } - }, - }, - - template: ` - <div :class="cssClass"> - - <div - class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!isLoading && !shouldRenderEmptyState"> - <div class="fade-left"> - <i class="fa fa-angle-left" aria-hidden="true"></i> - </div> - <div class="fade-right"> - <i class="fa fa-angle-right" aria-hidden="true"></i> - </div> - <navigation-tabs - :scope="scope" - :count="state.count" - :paths="paths" /> - - <navigation-controls - :new-pipeline-path="newPipelinePath" - :has-ci-enabled="hasCiEnabled" - :help-page-path="helpPagePath" - :ciLintPath="ciLintPath" - :can-create-pipeline="canCreatePipelineParsed " /> - </div> - - <div class="content-list pipelines"> - - <loading-icon - label="Loading Pipelines" - size="3" - v-if="isLoading" - /> - - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" /> - - <error-state v-if="shouldRenderErrorState" /> - - <div - class="blank-state blank-state-no-icon" - v-if="shouldRenderNoPipelinesMessage"> - <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> - </div> - - <div - class="table-holder" - v-if="shouldRenderTable"> - - <pipelines-table-component - :pipelines="state.pipelines" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </div> - - <table-pagination - v-if="shouldRenderPagination" - :change="change" - :pageInfo="state.pageInfo" - /> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js new file mode 100644 index 00000000000..923d9bfb248 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipelines_bundle.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import PipelinesStore from './stores/pipelines_store'; +import pipelinesComponent from './components/pipelines.vue'; + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#pipelines-list-vue', + data() { + const store = new PipelinesStore(); + + return { + store, + }; + }, + components: { + pipelinesComponent, + }, + render(createElement) { + return createElement('pipelines-component', { + props: { + store: this.store, + }, + }); + }, +})); diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 4a3df2fd465..141333b2b4d 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -3,7 +3,7 @@ // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview -// (including the explanation of slash commands), and showing a warning when +// (including the explanation of quick actions), and showing a warning when // more than `x` users are referenced. // (function () { diff --git a/app/assets/javascripts/prometheus_metrics/constants.js b/app/assets/javascripts/prometheus_metrics/constants.js new file mode 100644 index 00000000000..50f1248456e --- /dev/null +++ b/app/assets/javascripts/prometheus_metrics/constants.js @@ -0,0 +1,5 @@ +export default { + EMPTY: 'empty', + LOADING: 'loading', + LIST: 'list', +}; diff --git a/app/assets/javascripts/prometheus_metrics/index.js b/app/assets/javascripts/prometheus_metrics/index.js new file mode 100644 index 00000000000..a0c43c5abe1 --- /dev/null +++ b/app/assets/javascripts/prometheus_metrics/index.js @@ -0,0 +1,6 @@ +import PrometheusMetrics from './prometheus_metrics'; + +$(() => { + const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + prometheusMetrics.loadActiveMetrics(); +}); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js new file mode 100644 index 00000000000..ef4d6df5138 --- /dev/null +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -0,0 +1,109 @@ +import PANEL_STATE from './constants'; + +export default class PrometheusMetrics { + constructor(wrapperSelector) { + this.backOffRequestCounter = 0; + + this.$wrapper = $(wrapperSelector); + + this.$monitoredMetricsPanel = this.$wrapper.find('.js-panel-monitored-metrics'); + this.$monitoredMetricsCount = this.$monitoredMetricsPanel.find('.js-monitored-count'); + this.$monitoredMetricsLoading = this.$monitoredMetricsPanel.find('.js-loading-metrics'); + this.$monitoredMetricsEmpty = this.$monitoredMetricsPanel.find('.js-empty-metrics'); + this.$monitoredMetricsList = this.$monitoredMetricsPanel.find('.js-metrics-list'); + + this.$missingEnvVarPanel = this.$wrapper.find('.js-panel-missing-env-vars'); + this.$panelToggle = this.$missingEnvVarPanel.find('.js-panel-toggle'); + this.$missingEnvVarMetricCount = this.$missingEnvVarPanel.find('.js-env-var-count'); + this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list'); + + this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('active-metrics'); + + this.$panelToggle.on('click', e => this.handlePanelToggle(e)); + } + + /* eslint-disable class-methods-use-this */ + handlePanelToggle(e) { + const $toggleBtn = $(e.currentTarget); + const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body'); + $currentPanelBody.toggleClass('hidden'); + if ($toggleBtn.hasClass('fa-caret-down')) { + $toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right'); + } else { + $toggleBtn.removeClass('fa-caret-right').addClass('fa-caret-down'); + } + } + + showMonitoringMetricsPanelState(stateName) { + switch (stateName) { + case PANEL_STATE.LOADING: + this.$monitoredMetricsLoading.removeClass('hidden'); + this.$monitoredMetricsEmpty.addClass('hidden'); + this.$monitoredMetricsList.addClass('hidden'); + break; + case PANEL_STATE.LIST: + this.$monitoredMetricsLoading.addClass('hidden'); + this.$monitoredMetricsEmpty.addClass('hidden'); + this.$monitoredMetricsList.removeClass('hidden'); + break; + default: + this.$monitoredMetricsLoading.addClass('hidden'); + this.$monitoredMetricsEmpty.removeClass('hidden'); + this.$monitoredMetricsList.addClass('hidden'); + break; + } + } + + populateActiveMetrics(metrics) { + let totalMonitoredMetrics = 0; + let totalMissingEnvVarMetrics = 0; + + metrics.forEach((metric) => { + this.$monitoredMetricsList.append(`<li>${metric.group}<span class="badge">${metric.active_metrics}</span></li>`); + totalMonitoredMetrics += metric.active_metrics; + if (metric.metrics_missing_requirements > 0) { + this.$missingEnvVarMetricsList.append(`<li>${metric.group}</li>`); + totalMissingEnvVarMetrics += 1; + } + }); + + this.$monitoredMetricsCount.text(totalMonitoredMetrics); + this.showMonitoringMetricsPanelState(PANEL_STATE.LIST); + + if (totalMissingEnvVarMetrics > 0) { + this.$missingEnvVarPanel.removeClass('hidden'); + this.$missingEnvVarPanel.find('.flash-container').off('click'); + this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics); + } + } + + loadActiveMetrics() { + this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); + gl.utils.backOff((next, stop) => { + $.getJSON(this.activeMetricsEndpoint) + .done((res) => { + if (res && res.success) { + stop(res); + } else { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + if (this.backOffRequestCounter < 3) { + next(); + } else { + stop(res); + } + } + }) + .fail(stop); + }) + .then((res) => { + if (res && res.data && res.data.length) { + this.populateActiveMetrics(res.data); + } else { + this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + } + }) + .catch(() => { + this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + }); + } +} diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b71c3097706..322162afdb8 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -7,6 +7,11 @@ import Cookies from 'js-cookie'; function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); + + this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); + this.$navGitlab = $('.navbar-gitlab'); + this.$rightSidebar = $('.js-right-sidebar'); + this.removeListeners(); this.addEventListeners(); } @@ -21,14 +26,15 @@ import Cookies from 'js-cookie'; Sidebar.prototype.addEventListeners = function() { const $document = $(document); - const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10); + const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); + const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => throttledSetSidebarHeight()); + $document.on('scroll', () => debouncedSetSidebarHeight()); $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -207,13 +213,14 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight(); - const $rightSidebar = $('.js-right-sidebar'); + const $navHeight = this.$navGitlab.outerHeight(); const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { - $rightSidebar.outerHeight($(window).height() - diff); + this.$rightSidebar.outerHeight($(window).height() - diff); + this.$sidebarInner.height('100%'); } else { - $rightSidebar.outerHeight('100%'); + this.$rightSidebar.outerHeight('100%'); + this.$sidebarInner.height(''); } }; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index e67f449e1a2..7fa5996d600 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,11 +1,28 @@ +function expandSectionParent($section, $content) { + $section.addClass('expanded'); + $content.off('animationend.expandSectionParent'); +} + function expandSection($section) { - $section.find('.js-settings-toggle').text('Close'); - $section.find('.settings-content').addClass('expanded').off('scroll').scrollTop(0); + $section.find('.js-settings-toggle').text('Collapse'); + + const $content = $section.find('.settings-content'); + $content.addClass('expanded').off('scroll.expandSection').scrollTop(0); + + if ($content.hasClass('no-animate')) { + expandSectionParent($section, $content); + } else { + $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content)); + } } function closeSection($section) { $section.find('.js-settings-toggle').text('Expand'); - $section.find('.settings-content').removeClass('expanded').on('scroll', () => expandSection($section)); + + const $content = $section.find('.settings-content'); + $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section)); + + $section.removeClass('expanded'); } function toggleSection($section) { @@ -21,7 +38,7 @@ function toggleSection($section) { export default function initSettingsPanels() { $('.settings').each((i, elm) => { const $section = $(elm); - $section.on('click', '.js-settings-toggle', () => toggleSection($section)); - $section.find('.settings-content:not(.expanded)').on('scroll', () => expandSection($section)); + $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); + $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); }); } diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js index a9ad3708514..5a6e47e566e 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js @@ -14,6 +14,11 @@ export default { type: Boolean, required: true, }, + showToggle: { + type: Boolean, + required: false, + default: false, + }, }, computed: { assigneeTitle() { @@ -36,6 +41,19 @@ export default { > Edit </a> + <a + v-if="showToggle" + aria-label="Toggle sidebar" + class="gutter-toggle pull-right js-sidebar-toggle" + href="#" + role="button" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-angle-double-right" + /> + </a> </div> `, }; diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index da4abf0b68f..f83c3b037ed 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -64,6 +64,7 @@ export default { }, beforeMount() { this.field = this.$el.dataset.field; + this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined'; }, template: ` <div> @@ -71,6 +72,7 @@ export default { :number-of-assignees="store.assignees.length" :loading="loading || store.isFetching.assignees" :editable="store.editable" + :show-toggle="!signedIn" /> <assignees v-if="!store.isFetching.assignees" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js index b2a77462fe0..142ad437509 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js @@ -15,10 +15,10 @@ export default { <div class="time-tracking-help-state"> <div class="time-tracking-info"> <h4> - Track time with slash commands + Track time with quick actions </h4> <p> - Slash commands can be used in the issues description and comment boxes. + Quick actions can be used in the issues description and comment boxes. </p> <p> <code> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js index 244b67b3ad9..650e935b116 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js @@ -16,10 +16,10 @@ export default { 'issuable-time-tracker': timeTracker, }, methods: { - listenForSlashCommands() { - $(document).on('ajax:success', '.gfm-form', this.slashCommandListened); + listenForQuickActions() { + $(document).on('ajax:success', '.gfm-form', this.quickActionListened); }, - slashCommandListened(e, data) { + quickActionListened(e, data) { const subscribedCommands = ['spend_time', 'time_estimate']; let changedCommands; if (data !== undefined) { @@ -35,7 +35,7 @@ export default { }, }, mounted() { - this.listenForSlashCommands(); + this.listenForQuickActions(); }, template: ` <div class="block"> diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index ec45253e50b..46efdcf4202 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -643,7 +643,7 @@ UsersSelect.prototype.formatResult = function(user) { } else { avatar = gon.default_avatar_url; } - return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>"; + return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + ("@" + user.username || "") + "</div> </div>"; }; UsersSelect.prototype.formatSelection = function(user) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 8155218681c..76cb71b6c12 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -1,5 +1,5 @@ -import statusCodes from '~/lib/utils/http_status'; -import { bytesToMiB } from '~/lib/utils/number_utils'; +import statusCodes from '../../lib/utils/http_status'; +import { bytesToMiB } from '../../lib/utils/number_utils'; import MemoryGraph from '../../vue_shared/components/memory_graph'; import MRWidgetService from '../services/mr_widget_service'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js index c02e10128e2..e8b3cf2f729 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js @@ -17,6 +17,9 @@ export default { return hasCI && !ciStatus; }, + hasPipeline() { + return Object.keys(this.mr.pipeline || {}).length > 0; + }, svg() { return statusIconEntityMap.icon_status_failed; }, @@ -30,7 +33,11 @@ export default { template: ` <div class="mr-widget-heading"> <div class="ci-widget"> - <template v-if="hasCIError"> + <template v-if="!hasPipeline"> + <i class="fa fa-spinner fa-spin append-right-10" aria-hidden="true"></i> + Waiting for pipeline... + </template> + <template v-else-if="hasCIError"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error"> <span class="js-icon-link icon-link"> <span diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js deleted file mode 100644 index ff5ae28e062..00000000000 --- a/app/assets/javascripts/vue_shared/components/commit.js +++ /dev/null @@ -1,159 +0,0 @@ -import commitIconSvg from 'icons/_icon_commit.svg'; -import userAvatarLink from './user_avatar/user_avatar_link.vue'; - -export default { - props: { - /** - * Indicates the existance of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, - * if false will render `fa-code-fork` icon. - */ - tag: { - type: Boolean, - required: false, - default: false, - }, - - /** - * If provided is used to render the branch name and url. - * Should contain the following properties: - * name - * ref_url - */ - commitRef: { - type: Object, - required: false, - default: () => ({}), - }, - - /** - * Used to link to the commit sha. - */ - commitUrl: { - type: String, - required: false, - default: '', - }, - - /** - * Used to show the commit short sha that links to the commit url. - */ - shortSha: { - type: String, - required: false, - default: '', - }, - - /** - * If provided shows the commit tile. - */ - title: { - type: String, - required: false, - default: '', - }, - - /** - * If provided renders information about the author of the commit. - * When provided should include: - * `avatar_url` to render the avatar icon - * `web_url` to link to user profile - * `username` to render alt and title tags - */ - author: { - type: Object, - required: false, - default: () => ({}), - }, - }, - - computed: { - /** - * Used to verify if all the properties needed to render the commit - * ref section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; - }, - - /** - * Used to verify if all the properties needed to render the commit - * author section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasAuthor() { - return this.author && - this.author.avatar_url && - this.author.path && - this.author.username; - }, - - /** - * If information about the author is provided will return a string - * to be rendered as the alt attribute of the img tag. - * - * @returns {String} - */ - userImageAltDescription() { - return this.author && - this.author.username ? `${this.author.username}'s avatar` : null; - }, - }, - - data() { - return { commitIconSvg }; - }, - - components: { - userAvatarLink, - }, - template: ` - <div class="branch-commit"> - - <div v-if="hasCommitRef" class="icon-container"> - <i v-if="tag" class="fa fa-tag"></i> - <i v-if="!tag" class="fa fa-code-fork"></i> - </div> - - <a v-if="hasCommitRef" - class="ref-name" - :href="commitRef.ref_url"> - {{commitRef.name}} - </a> - - <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> - - <a class="commit-sha" - :href="commitUrl"> - {{shortSha}} - </a> - - <div class="commit-title flex-truncate-parent"> - <span v-if="title" class="flex-truncate-child"> - <user-avatar-link - v-if="hasAuthor" - class="avatar-image-container" - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="userImageAltDescription" - :tooltip-text="author.username" - /> - <a class="commit-row-message" - :href="commitUrl"> - {{title}} - </a> - </span> - <span v-else> - Cant find HEAD commit for this branch - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue new file mode 100644 index 00000000000..262584769e0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -0,0 +1,166 @@ +<script> + import commitIconSvg from 'icons/_icon_commit.svg'; + import userAvatarLink from './user_avatar/user_avatar_link.vue'; + + export default { + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render `fa-code-fork` icon. + */ + tag: { + type: Boolean, + required: false, + default: false, + }, + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + commitRef: { + type: Object, + required: false, + default: () => ({}), + }, + /** + * Used to link to the commit sha. + */ + commitUrl: { + type: String, + required: false, + default: '', + }, + + /** + * Used to show the commit short sha that links to the commit url. + */ + shortSha: { + type: String, + required: false, + default: '', + }, + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; + }, + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && + this.author.avatar_url && + this.author.path && + this.author.username; + }, + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && + this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, + data() { + return { commitIconSvg }; + }, + components: { + userAvatarLink, + }, + }; +</script> +<template> + <div class="branch-commit"> + <div v-if="hasCommitRef" class="icon-container hidden-xs"> + <i + v-if="tag" + class="fa fa-tag" + aria-hidden="true"> + </i> + <i + v-if="!tag" + class="fa fa-code-fork" + aria-hidden="true"> + </i> + </div> + + <a + v-if="hasCommitRef" + class="ref-name hidden-xs" + :href="commitRef.ref_url"> + {{commitRef.name}} + </a> + + <div + v-html="commitIconSvg" + class="commit-icon js-commit-icon"> + </div> + + <a + class="commit-sha" + :href="commitUrl"> + {{shortSha}} + </a> + + <div class="commit-title flex-truncate-parent"> + <span + v-if="title" + class="flex-truncate-child"> + <user-avatar-link + v-if="hasAuthor" + class="avatar-image-container" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="userImageAltDescription" + :tooltip-text="author.username" + /> + <a class="commit-row-message" + :href="commitUrl"> + {{title}} + </a> + </span> + <span v-else> + Cant find HEAD commit for this branch + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 1d4d90f75b6..bdc059f4a03 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -2,7 +2,7 @@ import ciIconBadge from './ci_badge_link.vue'; import loadingIcon from './loading_icon.vue'; import timeagoTooltip from './time_ago_tooltip.vue'; -import tooltipMixin from '../mixins/tooltip'; +import tooltip from '../directives/tooltip'; import userAvatarImage from './user_avatar/user_avatar_image.vue'; /** @@ -47,9 +47,9 @@ export default { }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, components: { ciIconBadge, @@ -90,10 +90,10 @@ export default { <template v-if="user"> <a + v-tooltip :href="user.path" :title="user.email" - class="js-user-link commit-committer-link" - ref="tooltip"> + class="js-user-link commit-committer-link"> <user-avatar-image :img-src="user.avatar_url" diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 41b1d0165b0..15581d5c2a0 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -12,9 +12,18 @@ required: false, default: '1', }, + + inline: { + type: Boolean, + required: false, + default: false, + }, }, computed: { + rootElementType() { + return this.inline ? 'span' : 'div'; + }, cssClass() { return `fa-${this.size}x`; }, @@ -22,12 +31,14 @@ }; </script> <template> - <div class="text-center"> + <component + :is="this.rootElementType" + class="text-center"> <i class="fa fa-spin fa-spinner" :class="cssClass" aria-hidden="true" :aria-label="label"> </i> - </div> + </component> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 1a11f493b7f..5bf2a90cc3b 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,17 +1,17 @@ <script> - import tooltipMixin from '../../mixins/tooltip'; + import tooltip from '../../directives/tooltip'; import toolbarButton from './toolbar_button.vue'; export default { - mixins: [ - tooltipMixin, - ], props: { previewMarkdown: { type: Boolean, required: true, }, }, + directives: { + tooltip, + }, components: { toolbarButton, }, @@ -94,13 +94,13 @@ </div> <div class="toolbar-group"> <button + v-tooltip aria-label="Go full screen" class="toolbar-btn js-zen-enter" data-container="body" tabindex="-1" title="Go full screen" - type="button" - ref="tooltip"> + type="button"> <i aria-hidden="true" class="fa fa-arrows-alt fa-fw"> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 096be507625..f7da7ebfcfe 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,10 +1,7 @@ <script> - import tooltipMixin from '../../mixins/tooltip'; + import tooltip from '../../directives/tooltip'; export default { - mixins: [ - tooltipMixin, - ], props: { buttonTitle: { type: String, @@ -29,6 +26,9 @@ default: false, }, }, + directives: { + tooltip, + }, computed: { iconClass() { return `fa-${this.icon}`; @@ -39,10 +39,10 @@ <template> <button + v-tooltip type="button" class="toolbar-btn js-md hidden-xs" tabindex="-1" - ref="tooltip" data-container="body" :data-md-tag="tag" :data-md-block="tagBlock" diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js deleted file mode 100644 index 48a39f18112..00000000000 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js +++ /dev/null @@ -1,55 +0,0 @@ -import PipelinesTableRowComponent from './pipelines_table_row'; - -/** - * Pipelines Table Component. - * - * Given an array of objects, renders a table. - */ -export default { - props: { - pipelines: { - type: Array, - required: true, - }, - - service: { - type: Object, - required: true, - }, - - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, - }, - }, - - components: { - 'pipelines-table-row-component': PipelinesTableRowComponent, - }, - - template: ` - <table class="table ci-table"> - <thead> - <tr> - <th class="js-pipeline-status pipeline-status">Status</th> - <th class="js-pipeline-info pipeline-info">Pipeline</th> - <th class="js-pipeline-commit pipeline-commit">Commit</th> - <th class="js-pipeline-stages pipeline-stages">Stages</th> - <th class="js-pipeline-date pipeline-date"></th> - <th class="js-pipeline-actions pipeline-actions"></th> - </tr> - </thead> - <tbody> - <template v-for="model in pipelines" - v-bind:model="model"> - <tr is="pipelines-table-row-component" - :pipeline="model" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </template> - </tbody> - </table> - `, -}; diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index 1c6ef071a6d..3ff7f6e2c4e 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -1,5 +1,5 @@ <script> -import tooltipMixin from '../mixins/tooltip'; +import tooltip from '../directives/tooltip'; import timeagoMixin from '../mixins/timeago'; import '../../lib/utils/datetime_utility'; @@ -28,19 +28,21 @@ export default { }, mixins: [ - tooltipMixin, timeagoMixin, ], + + directives: { + tooltip, + }, }; </script> <template> <time + v-tooltip :class="cssClass" - class="js-vue-timeago" :title="tooltipTitle(time)" :data-placement="tooltipPlacement" - data-container="body" - ref="tooltip"> + data-container="body"> {{timeFormated(time)}} </time> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index cd6f8c7aee4..dd9a2ebb184 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -16,11 +16,10 @@ */ import defaultAvatarUrl from 'images/no_avatar.png'; -import TooltipMixin from '../../mixins/tooltip'; +import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarImage', - mixins: [TooltipMixin], props: { imgSrc: { type: String, @@ -53,6 +52,9 @@ export default { default: 'top', }, }, + directives: { + tooltip, + }, computed: { tooltipContainer() { return this.tooltipText ? 'body' : null; @@ -72,6 +74,7 @@ export default { <template> <img + v-tooltip class="avatar" :class="[avatarSizeClass, cssClasses]" :src="imageSource" @@ -81,6 +84,5 @@ export default { :data-container="tooltipContainer" :data-placement="tooltipPlacement" :title="tooltipText" - ref="tooltip" /> </template> diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js new file mode 100644 index 00000000000..dc896cf5c7d --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -0,0 +1,13 @@ +export default { + bind(el) { + $(el).tooltip(); + }, + + componentUpdated(el) { + $(el).tooltip('fixTitle'); + }, + + unbind(el) { + $(el).tooltip('destroy'); + }, +}; diff --git a/app/assets/javascripts/vue_shared/mixins/tooltip.js b/app/assets/javascripts/vue_shared/mixins/tooltip.js deleted file mode 100644 index 995c0c98505..00000000000 --- a/app/assets/javascripts/vue_shared/mixins/tooltip.js +++ /dev/null @@ -1,13 +0,0 @@ -export default { - mounted() { - $(this.$refs.tooltip).tooltip(); - }, - - updated() { - $(this.$refs.tooltip).tooltip('fixTitle'); - }, - - beforeDestroy() { - $(this.$refs.tooltip).tooltip('destroy'); - }, -}; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index fefe5575d9b..95a08c960ea 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -254,7 +254,7 @@ } .landing { - margin-bottom: $gl-padding; + margin: $gl-padding auto; overflow: hidden; display: flex; position: relative; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cba890ce831..4f54ca24940 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -395,6 +395,11 @@ .dropdown-menu-align-right { left: auto; right: 0; + margin-top: -5px; + + @media (max-width: $screen-xs-max) { + left: 0; + } } .dropdown-menu-selectable { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index d08df05fd6c..245117b2559 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -12,6 +12,12 @@ &.readme-holder { margin: $gl-padding 0; + + &.limited-width-container .file-content { + max-width: $limited-layout-width-sm; + margin-left: auto; + margin-right: auto; + } } table { @@ -59,6 +65,44 @@ } } + .file-blame-legend { + background-color: $gray-light; + text-align: right; + padding: 8px $gl-padding; + border-bottom: 1px solid $border-color; + + @media (max-width: $screen-xs-max) { + text-align: left; + } + + .left-label { + padding-right: 5px; + } + + .right-label { + padding-left: 5px; + } + + .legend-box { + display: inline-block; + width: 10px; + height: 10px; + padding: 0 2px; + } + + @for $i from 0 through 5 { + .legend-box-#{$i} { + background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + } + } + + @for $i from 1 through 4 { + .legend-box-#{$i + 5} { + background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + } + } + } + .file-content { background: $white-light; @@ -85,7 +129,7 @@ } /** - * Annotate file + * Blame file */ &.blame { table { @@ -118,6 +162,19 @@ padding: 5px 10px; min-width: 400px; background: $gray-light; + border-left: 3px solid; + } + + @for $i from 0 through 5 { + td.blame-commit-age-#{$i} { + border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + } + } + + @for $i from 1 through 4 { + td.blame-commit-age-#{$i + 5} { + border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + } } td.line-numbers { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index cfbaaaa04c7..767cf5ffea5 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -152,7 +152,7 @@ } .value-container { - background-color: $filter-value-selected-color; + box-shadow: inset 0 0 0 100px $filtered-search-term-shadow-color; } } @@ -236,9 +236,6 @@ width: 35px; background-color: $white-light; border: none; - position: static; - right: 0; - height: 100%; outline: none; z-index: 1; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index a78179e727f..61e3897f369 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -125,10 +125,11 @@ label { .select-wrapper { position: relative; - .fa-caret-down { + .fa-chevron-down { position: absolute; + font-size: 10px; right: 10px; - top: 10px; + top: 12px; color: $gray-darkest; pointer-events: none; } @@ -138,6 +139,12 @@ label { padding-left: 10px; padding-right: 10px; -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + &::-ms-expand { + display: none; + } } .form-control-inline { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index d8645afb7da..5bd6c095109 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -34,6 +34,8 @@ header { top: 0; left: 0; right: 0; + color: $gl-text-color-secondary; + border-radius: 0; @media (max-width: $screen-xs-min) { padding: 0 16px; @@ -59,7 +61,7 @@ header { padding: 0; .nav > li > a { - color: $gl-text-color-secondary; + color: currentColor; font-size: 18px; padding: 0; margin: (($header-height - 28) / 2) 3px; @@ -84,7 +86,7 @@ header { &:hover, &:focus, &:active { - background-color: $gray-light; + background-color: transparent; color: $gl-text-color; svg { @@ -96,13 +98,19 @@ header { font-size: 14px; } + .fa-chevron-down { + position: relative; + top: -3px; + font-size: 10px; + } + svg { position: relative; top: 2px; height: 17px; // hack to get SVG to line up with FA icons width: 23px; - fill: $gl-text-color-secondary; + fill: currentColor; } } @@ -225,7 +233,7 @@ header { } a { - color: $gl-text-color; + color: currentColor; &:hover { text-decoration: underline; @@ -346,6 +354,8 @@ header { width: auto; min-width: 140px; margin-top: -5px; + color: $gl-text-color; + left: auto; .current-user { padding: 5px 18px; diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 49bff23452d..4a9d41b4fda 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -53,7 +53,7 @@ body { } &.limit-container-width-sm { - max-width: 790px; + max-width: $limited-layout-width-sm; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 80691a234f8..b21bcc22a87 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -174,3 +174,14 @@ white-space: nowrap; } } + +@media(max-width: $screen-xs-max) { + .atwho-view-ul { + width: 350px; + } + + .atwho-view ul li { + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 3787ef370b2..28b2a7cfacd 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -45,8 +45,7 @@ li { display: flex; - a, - .btn-link { + a { padding: $gl-btn-padding; padding-bottom: 11px; font-size: 14px; @@ -68,29 +67,7 @@ } } - .btn-link { - padding-top: 16px; - padding-left: 15px; - padding-right: 15px; - border-left: none; - border-right: none; - border-top: none; - border-radius: 0; - - &:hover, - &:active, - &:focus { - background-color: transparent; - } - - &:active { - outline: 0; - box-shadow: none; - } - } - - &.active a, - &.active .btn-link { + &.active a { border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: 600; diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index 5f4211147f3..f1ecd050a0a 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -59,4 +59,8 @@ margin: 0 2px 0 3px; } } + + .ci-status { + margin-right: 10px; + } } diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 9d8d08dff88..e8d69e62194 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -1,50 +1,60 @@ .panel { margin-bottom: $gl-padding; +} + +.panel-slim { + @extend .panel; + margin-bottom: $gl-vert-padding; +} + + +.panel-heading { + padding: $gl-vert-padding $gl-padding; + line-height: 36px; + + .controls { + margin-top: -2px; + float: right; + } + + .dropdown-menu-toggle { + line-height: 20px; + } - .panel-heading { - padding: $gl-vert-padding $gl-padding; - line-height: 36px; - - .controls { - margin-top: -2px; - float: right; - } - - .dropdown-menu-toggle { - line-height: 20px; - } - - .badge { - margin-top: -2px; - margin-left: 5px; - } - - &.split { - display: flex; - align-items: center; - } - - .left { - flex: 1 1 auto; - } - - .right { - flex: 0 0 auto; - text-align: right; - } + .badge { + margin-top: -2px; + margin-left: 5px; } - .panel-body { - padding: $gl-padding; + &.split { + display: flex; + align-items: center; + } - .form-actions { - margin: -$gl-padding; - margin-top: $gl-padding; - } + .left { + flex: 1 1 auto; } - .panel-title { - font-size: inherit; - line-height: inherit; + .right { + flex: 0 0 auto; + text-align: right; } } + +.panel-empty-heading { + border-bottom: 0; +} + +.panel-body { + padding: $gl-padding; + + .form-actions { + margin: -$gl-padding; + margin-top: $gl-padding; + } +} + +.panel-title { + font-size: inherit; + line-height: inherit; +} diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive-tables.scss index f0a4c66aa1a..d2c90908baa 100644 --- a/app/assets/stylesheets/framework/responsive-tables.scss +++ b/app/assets/stylesheets/framework/responsive-tables.scss @@ -36,13 +36,58 @@ align-self: stretch; padding: 10px; align-items: center; - height: 62px; + min-height: 62px; &:not(:first-of-type) { border-top: 1px solid $white-normal; } } } + + &.section-wrap { + white-space: normal; + + @media (max-width: $screen-sm-max) { + flex-wrap: wrap; + } + } + } +} + + +.table-button-footer { + @media (min-width: $screen-md-min) { + text-align: right; + } + + @media (max-width: $screen-sm-max) { + background-color: $gray-normal; + align-self: stretch; + border-top: 1px solid $border-color; + + .table-action-buttons { + padding: 10px 5px; + display: flex; + + .btn { + border-radius: 3px; + } + + > .btn-group, + > .external-url, + > .btn { + flex: 1 1 28px; + margin: 0 5px; + } + + .dropdown-new { + width: 100%; + } + + .dropdown-menu { + min-width: initial; + } + } } } @@ -56,6 +101,7 @@ .table-mobile-header { color: $gl-text-color-secondary; + text-align: left; @include flex-max-width(40); @media (min-width: $screen-md-min) { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 5ae833cd5f6..40e654f4838 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -18,19 +18,28 @@ background-image: none; background-color: transparent; border: none; - padding-top: 6px; - padding-right: 10px; + padding-top: 12px; + padding-right: 20px; + font-size: 10px; b { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 5px dashed; - border-right: 5px solid transparent; - border-left: 5px solid transparent; + display: none; + } + + &::after { + content: "\f078"; + position: absolute; + z-index: 1; + text-align: center; + pointer-events: none; + box-sizing: border-box; color: $gray-darkest; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } } @@ -109,10 +118,12 @@ line-height: 15px; background-color: $gray-light; background-image: none; + padding: 3px 18px 3px 5px; .select2-search-choice-close { - top: 4px; - left: 3px; + top: 5px; + left: initial; + right: 3px; } &.select2-search-choice-focus { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d4421e3af74..5cf9330b8f8 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -97,17 +97,19 @@ .issues-bulk-update.right-sidebar { @include maintain-sidebar-dimensions; - transition: right $sidebar-transition-duration; - right: -$gutter-width; + width: 0; + padding: 0; + transition: width $sidebar-transition-duration; &.right-sidebar-expanded { @include maintain-sidebar-dimensions; - right: 0; + width: $gutter-width; } &.right-sidebar-collapsed { @include maintain-sidebar-dimensions; - right: -$gutter-width; + width: 0; + padding: 0; .block { padding: 16px 0; @@ -118,5 +120,6 @@ .issuable-sidebar { padding: 0 3px; + width: calc(100% + 35px); } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 10881987038..3d68a50f91f 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -44,6 +44,10 @@ &:target, &.target { background: $line-target-blue; + + &.system-note .note-body .note-text.system-note-commit-list::after { + background: linear-gradient(rgba($line-target-blue, 0.1) -100px, $line-target-blue 100%); + } } .avatar { diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index c9f345d24be..b666223b120 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -74,9 +74,9 @@ $pagination-hover-color: $gl-text-color; $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; -$pagination-active-color: $blue-600; -$pagination-active-bg: $white-light; -$pagination-active-border: $border-color; +$pagination-active-color: $white-light; +$pagination-active-bg: $gl-link-color; +$pagination-active-border: $gl-link-color; $pagination-disabled-color: #cdcdcd; $pagination-disabled-bg: $gray-light; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4114a050d9a..da4d91511e0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -74,6 +74,12 @@ $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; +$purple-600: #6e49cb; +$purple-650: #5c35ae; +$purple-700: #4a2192; +$purple-800: #2c0a5c; +$purple-900: #380d75; + $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); @@ -99,6 +105,7 @@ $well-light-text-color: #5b6169; */ $gl-font-size: 14px; $gl-text-color: rgba(0, 0, 0, .85); +$gl-text-color-light: rgba(0, 0, 0, .7); $gl-text-color-secondary: rgba(0, 0, 0, .55); $gl-text-color-disabled: rgba(0, 0, 0, .35); $gl-text-color-inverted: rgba(255, 255, 255, 1.0); @@ -161,6 +168,7 @@ $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; +$limited-layout-width-sm: 790px; $gl-avatar-size: 40px; $error-exclamation-point: $red-500; $border-radius-default: 3px; @@ -282,6 +290,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* * Filtered Search */ +$filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09); $dropdown-hover-color: $blue-400; /* @@ -322,6 +331,7 @@ $note-disabled-comment-color: #b2b2b2; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; +$note-icon-gutter-width: 55px; /* @@ -365,6 +375,13 @@ $avatar-border: rgba(0, 0, 0, .1); $gl-avatar-size: 40px; /* +* Blame +*/ +$blame-gray: #ededed; +$blame-cyan: #acd5f2; +$blame-blue: #254e77; + +/* * Builds */ $builds-trace-bg: #111; diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss deleted file mode 100644 index 9f613710cf4..00000000000 --- a/app/assets/stylesheets/mailers/devise.scss +++ /dev/null @@ -1,140 +0,0 @@ -@import "framework/variables"; - -// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout -// used for Devise email templates, and _should not_ be included in any -// application stylesheets. -// -// Styles defined here are embedded directly into the resulting email HTML via -// the `premailer` gem. - -$body-background-color: #363636; -$message-background-color: #fafafa; - -$header-color: #6b4fbb; -$body-color: #444; -$cta-color: #e14329; -$footer-link-color: #7e7e7e; - -$font-family: Helvetica, Arial, sans-serif; - -body { - background-color: $body-background-color; - font-family: $font-family; - margin: 0; - padding: 0; -} - -table { - -premailer-cellpadding: 0; - -premailer-cellspacing: 0; - - border: 0; - border-collapse: separate; - - &#wrapper { - background-color: $body-background-color; - width: 100%; - } - - &#header { - margin: 0 auto; - text-align: left; - width: 600px; - - & > td { - text-align: center; - } - } - - &#body { - background-color: $message-background-color; - border: 1px solid $black; - border-radius: 4px; - margin: 0 auto; - width: 600px; - } - - &#footer { - color: $footer-link-color; - font-size: 14px; - text-align: center; - width: 100%; - } - - td { - &#body-container { - padding: 20px 40px; - } - } -} - -.center { - text-align: center; -} - -#logo { - border: none; - outline: none; - min-height: 88px; - width: 134px; -} - -#content { - h2 { - color: $header-color; - font-size: 30px; - font-weight: 400; - line-height: 34px; - margin-top: 0; - } - - p { - color: $body-color; - font-size: 17px; - line-height: 24px; - margin-bottom: 0; - } -} - -#cta { - border: 1px solid $cta-color; - border-radius: 3px; - display: inline-block; - margin: 20px 0; - padding: 12px 24px; - - a { - background-color: $message-background-color; - color: $cta-color; - display: inline-block; - text-decoration: none; - } -} - -#tanuki { - padding: 40px 0 0; - - img { - border: none; - outline: none; - width: 37px; - min-height: 36px; - } -} - -#tagline { - font-size: 22px; - font-weight: 100; - padding: 4px 0 40px; -} - -#social { - padding: 0 10px 20px; - width: 600px; - word-spacing: 20px; - - a { - color: $footer-link-color; - text-decoration: none; - } -} diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss new file mode 100644 index 00000000000..441bfc479f6 --- /dev/null +++ b/app/assets/stylesheets/new_nav.scss @@ -0,0 +1,267 @@ +@import "framework/variables"; +@import 'framework/tw_bootstrap_variables'; +@import "bootstrap/variables"; + +header.navbar-gitlab-new { + color: $white-light; + background-color: $purple-900; + border-bottom: 0; + + .header-content { + padding-left: 0; + + .title-container { + padding-top: 0; + overflow: visible; + } + + .title { + display: block; + height: 100%; + padding-right: 0; + color: currentColor; + + > a { + display: flex; + align-items: center; + height: 100%; + padding-top: 3px; + padding-right: $gl-padding; + padding-left: $gl-padding; + margin-left: -$gl-padding; + border-bottom: 3px solid transparent; + + @media (min-width: $screen-sm-min) { + padding-right: $gl-padding; + padding-left: $gl-padding; + } + + svg { + margin-top: -3px; + + @media (min-width: $screen-sm-min) { + margin-right: 10px; + } + } + + &:hover, + &:focus { + color: currentColor; + text-decoration: none; + border-bottom-color: $white-light; + } + } + } + + .dropdown.open { + > a { + border-bottom-color: $white-light; + } + } + + .dropdown-menu { + margin-top: 4px; + min-width: 130px; + + @media (max-width: $screen-xs-max) { + left: auto; + right: 0; + } + } + } + + .navbar-collapse { + padding-left: 0; + color: $white-light; + box-shadow: 0; + + @media (max-width: $screen-xs-max) { + margin-left: -$gl-padding; + margin-right: -10px; + } + + .dropdown-bold-header { + color: initial; + } + + .nav { + > li:not(.hidden-xs) a { + @media (max-width: $screen-xs-max) { + margin-left: 0; + min-width: 100%; + } + } + } + } + + .container-fluid { + .navbar-toggle { + min-width: 45px; + padding: 6px $gl-padding; + margin-right: -7px; + font-size: 14px; + text-align: center; + color: currentColor; + border-left: 1px solid lighten($purple-700, 10%); + + &:hover, + &:focus, + &.active { + color: currentColor; + background-color: transparent; + } + } + + .navbar-nav { + @media (max-width: $screen-xs-max) { + display: flex; + padding-right: 10px; + } + + li { + .badge { + box-shadow: none; + } + } + } + + .nav > li { + &.header-user { + @media (max-width: $screen-xs-max) { + padding-left: 10px; + } + } + + > a { + background: none; + opacity: .9; + will-change: opacity; + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } + } + + &:hover, + &:focus { + color: $white-light; + opacity: 1; + + > svg { + fill: $white-light; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } + } + } + } + } + } +} + +.navbar-sub-nav { + display: flex; + margin-bottom: 0; + color: $white-light; + + > li { + &.active > a, + a:hover, + a:focus { + border-bottom-color: $white-light; + text-decoration: none; + outline: 0; + opacity: 1; + } + + > a { + display: block; + padding: 16px 10px 13px; + font-size: 13px; + color: currentColor; + border-bottom: 3px solid transparent; + opacity: .9; + will-change: opacity; + + @media (min-width: $screen-sm-min) { + padding: 15px $gl-padding 12px; + font-size: 14px; + } + } + } + + .dropdown-chevron { + position: relative; + top: -1px; + font-size: 10px; + } +} + +.header-user .dropdown-menu-nav, +.header-new .dropdown-menu-nav { + margin-top: 4px; +} + +.search { + form { + border-color: $purple-800; + + &:hover { + border-color: rgba($white-light, .6); + box-shadow: none; + } + } + + &.search-active form { + border-color: $white-light; + } + + form, + .search-input { + background-color: $purple-700; + } + + .search-input { + color: $white-light; + } + + .search-input::placeholder { + color: rgba($white-light, .6); + } + + .location-badge { + font-size: 12px; + color: rgba($white-light, .6); + background-color: $purple-800; + transition: color 0.15s; + will-change: color; + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: rgba($white-light, .6); + } + } + + &.search-active { + .location-badge { + color: $white-light; + background-color: $purple-800; + } + + .search-input-wrap { + .search-icon { + color: rgba($white-light, .6); + } + + .clear-icon { + color: $white-light; + } + } + } +} diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss new file mode 100644 index 00000000000..06c6025ed6b --- /dev/null +++ b/app/assets/stylesheets/new_sidebar.scss @@ -0,0 +1,121 @@ +@import "framework/variables"; +@import 'framework/tw_bootstrap_variables'; +@import "bootstrap/variables"; + +$new-sidebar-width: 220px; + +.page-with-new-sidebar { + + @media (min-width: $screen-sm-min) { + padding-left: $new-sidebar-width; + } + + .right-sidebar { + position: fixed; + height: 100%; + } +} + +.nav-sidebar { + position: fixed; + z-index: 400; + width: $new-sidebar-width; + top: 50px; + bottom: 0; + left: 0; + overflow: auto; + background-color: $gray-light; + border-right: 1px solid $border-color; + + ul { + padding: 0; + list-style: none; + } + + li { + a { + display: block; + padding: 12px 14px; + } + } + + a { + color: $gl-text-color; + text-decoration: none; + } +} + +.sidebar-sub-level-items { + display: none; + + > li { + a { + padding: 12px 24px; + color: $gl-text-color-light; + + &:hover { + color: $gl-text-color; + background-color: $border-color; + } + } + + &.active { + > a { + color: $purple-650; + font-weight: 600; + } + } + } +} + +.sidebar-top-level-items { + > li { + .badge { + float: right; + background-color: $border-color; + color: $gl-text-color; + } + + &.active { + > a { + background-color: $purple-600; + color: $white-light; + font-weight: 600; + } + + .badge { + background-color: $purple-700; + color: $white-light; + } + + .sidebar-sub-level-items { + background-color: $gray-normal; + border-left: 6px solid $purple-600; + display: block; + } + } + + &:not(.active) > a:hover { + background-color: $border-color; + + .badge { + transition: background-color 100ms linear; + background-color: $gray-normal; + } + } + } +} + + +// Make issue boards full-height now that sub-nav is gone + +.boards-list { + height: calc(100vh - 50px); + + @media (min-width: $screen-sm-min) { + height: 475px; // Needed for PhantomJS + // scss-lint:disable DuplicateProperty + height: calc(100vh - 120px); + // scss-lint:enable DuplicateProperty + } +} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 740e383dbb5..85109fec91a 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,3 +1,5 @@ +@import "./issues/issue_count_badge"; + [v-cloak] { display: none; } @@ -133,7 +135,7 @@ } .board-list-component, - .board-issue-count-holder { + .issue-count-badge { display: none; } } @@ -429,30 +431,6 @@ margin: 5px; } -.board-issue-count-holder { - margin-top: -3px; - - .btn { - line-height: 12px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } -} - -.board-issue-count { - padding-right: 10px; - padding-left: 10px; - line-height: 21px; - border-radius: $border-radius-base; - border: 1px solid $border-color; - - &.has-btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-width: 1px 0 1px 1px; - } -} - .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { &.right-sidebar { top: 0; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 39022714d28..7eee0a71c66 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -150,6 +150,7 @@ overflow-y: scroll; overflow-x: hidden; padding: 10px 20px 20px 5px; + white-space: pre; } .environment-information { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 7bec4bd5f56..3039732ca5b 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -4,7 +4,7 @@ position: relative; .landing { - margin-top: 10px; + margin-top: 0; .inner-content { white-space: normal; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index b24803678ea..1046ebfa2e2 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -83,6 +83,7 @@ .avatar { float: none; + margin-right: 0; } } @@ -124,43 +125,6 @@ } .gl-responsive-table-row { - .environments-actions { - @media (min-width: $screen-md-min) { - text-align: right; - } - - @media (max-width: $screen-sm-max) { - background-color: $gray-normal; - align-self: stretch; - border-top: 1px solid $border-color; - - .environment-action-buttons { - padding: 10px 5px; - display: flex; - - .btn { - border-radius: 3px; - } - - > .btn-group, - > .external-url, - > .btn { - flex: 1; - flex-basis: 28px; - margin: 0 5px; - } - - .dropdown-new { - width: 100%; - } - - .dropdown-menu { - min-width: initial; - } - } - } - } - .branch-commit { max-width: 100%; } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5b723f7c722..4c3fa1fb8d4 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -89,7 +89,6 @@ background: $gray-light; border-radius: 0; color: $events-pre-color; - margin: 0 20px; overflow: hidden; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 72d73b89a2a..6f6c6839975 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -90,8 +90,6 @@ } .explore-groups.landing { - margin-top: 10px; - .inner-content { padding: 0; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b3f310ff67d..e3ebcc8af6c 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -11,7 +11,9 @@ .commit-box, .info-well, .commit-ci-menu, - .files-changed { + .files-changed, + .limited-header-width, + .limited-width-notes { @extend .fixed-width-container; } @@ -204,7 +206,7 @@ .issuable-sidebar { width: calc(100% + 100px); - height: 100%; + height: calc(100% - #{$header-height}); overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; @@ -226,6 +228,12 @@ padding-top: 10px; } + &:not(.issue-boards-sidebar):not([data-signed-in]) { + .issuable-sidebar-header { + display: none; + } + } + .assign-yourself .btn-link { padding-left: 0; } @@ -247,6 +255,10 @@ border-left: 1px solid $border-gray-normal; } + .title .gutter-toggle { + margin-top: 0; + } + .assignee .avatar { float: left; margin-right: 10px; @@ -729,33 +741,3 @@ } } } - -.confidential-issue-warning { - background-color: $gl-gray; - border-radius: 3px; - padding: $gl-btn-padding $gl-padding; - margin-top: $gl-padding-top; - font-size: 14px; - color: $white-light; - - .fa { - margin-right: 8px; - } - - a { - color: $white-light; - text-decoration: underline; - } - - &.affix { - position: static; - width: initial; - - @media (min-width: $screen-sm-min) { - position: sticky; - position: -webkit-sticky; - top: 60px; - z-index: 200; - } - } -} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index f923a1104a9..8cdb3f34ae5 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -1,3 +1,5 @@ +@import "./issues/issue_count_badge"; + .issues-list { .issue { padding: 10px 0 10px $gl-padding; diff --git a/app/assets/stylesheets/pages/issues/issue_count_badge.scss b/app/assets/stylesheets/pages/issues/issue_count_badge.scss new file mode 100644 index 00000000000..ccb62bfed18 --- /dev/null +++ b/app/assets/stylesheets/pages/issues/issue_count_badge.scss @@ -0,0 +1,29 @@ +.issue-count-badge { + display: inline-flex; + align-items: stretch; + height: 24px; +} + +.issue-count-badge-count { + display: flex; + align-items: center; + padding-right: 10px; + padding-left: 10px; + border: 1px solid $border-color; + border-radius: $border-radius-base; + line-height: 1; + + &.has-btn { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +} + +.issue-count-badge-add-button { + display: flex; + align-items: center; + border: 1px solid $border-color; + border-radius: 0 $border-radius-base $border-radius-base 0; + line-height: 1; +} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index c10588ac58e..b158416b940 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -138,6 +138,7 @@ .fa { font-size: 18px; vertical-align: middle; + pointer-events: none; } &:hover { diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 4be0e133b69..f21005895e4 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -136,10 +136,6 @@ width: 250px; } - @media (min-width: $screen-md-min) { - width: 350px; - } - &.input-short { @media (min-width: $screen-md-min) { width: 170px; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2dc7f73a295..59e0624d94e 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -419,7 +419,7 @@ .commit { margin: 0; - padding: 10px 0; + padding: 10px; list-style: none; &:hover { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 335e587b8f4..55e0ee1936e 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -111,8 +111,8 @@ } } -.issues-sortable-list, -.merge_requests-sortable-list { +.milestone-issues-list, +.milestone-merge_requests-list { .issuable-detail { display: block; margin-top: 7px; @@ -197,6 +197,4 @@ .issuable-row { background-color: $white-light; - cursor: -webkit-grab; - cursor: grab; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index aa307414737..9877ed2cfd6 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -103,6 +103,42 @@ } } +.confidential-issue-warning { + background-color: $gray-normal; + border-radius: 3px; + padding: 3px 12px; + margin: auto; + margin-top: 0; + text-align: center; + font-size: 12px; + align-items: center; + + @media (max-width: $screen-md-max) { + // On smaller devices the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } + + .fa { + margin-right: 8px; + } +} + +.right-sidebar-expanded { + .confidential-issue-warning { + // When the sidebar is open the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } +} + + .discussion-form { padding: $gl-padding-top $gl-padding $gl-padding; background-color: $white-light; @@ -112,8 +148,20 @@ padding: 6px 0; } -.notes-form > li { - border: 0; +.notes.notes-form > li.timeline-entry { + @include notes-media('max', $screen-sm-max) { + padding: 0; + } + + .timeline-content { + @include notes-media('max', $screen-sm-max) { + margin: 0; + } + } + + .timeline-entry-inner { + border: 0; + } } .note-edit-form { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a0442463390..53d5cf2f7bc 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -14,16 +14,6 @@ ul.notes { margin: 0; padding: 0; - .timeline-content { - margin-left: 55px; - - &.timeline-content-form { - @include notes-media('max', $screen-sm-max) { - margin-left: 0; - } - } - } - .note-created-ago, .note-updated-at { white-space: nowrap; @@ -46,17 +36,49 @@ ul.notes { } } - > li { - padding: $gl-padding $gl-btn-padding; + > li { // .timeline-entry + padding: 0; display: block; position: relative; - border-bottom: 1px solid $white-normal; + border-bottom: 0; + + @include notes-media('min', $screen-sm-min) { + padding-left: $note-icon-gutter-width; + } - &:last-child { - // Override `.timeline > li:last-child { border-bottom: none; }` + .timeline-entry-inner { + padding: $gl-padding $gl-btn-padding; border-bottom: 1px solid $white-normal; } + &:target, + &.target { + border-bottom: 1px solid $white-normal; + + &:not(:first-child) { + border-top: 1px solid $white-normal; + margin-top: -1px; + } + + .timeline-entry-inner { + border-bottom: 0; + } + } + + .timeline-icon { + @include notes-media('min', $screen-sm-min) { + margin-left: -$note-icon-gutter-width; + } + } + + .timeline-content { + margin-left: $note-icon-gutter-width; + + @include notes-media('min', $screen-sm-min) { + margin-left: 0; + } + } + &.being-posted { pointer-events: none; opacity: 0.5; @@ -73,7 +95,7 @@ ul.notes { } &.note-discussion { - &.timeline-entry { + .timeline-entry-inner { padding: $gl-padding 10px; } } @@ -152,13 +174,8 @@ ul.notes { .system-note { font-size: 14px; - padding-left: 0; clear: both; - @include notes-media('min', $screen-sm-min) { - margin-left: 65px; - } - .note-header-info { padding-bottom: 0; } @@ -192,13 +209,16 @@ ul.notes { .timeline-icon { float: left; + @include notes-media('min', $screen-sm-min) { + margin-left: 0; + width: auto; + } + svg { width: 16px; height: 16px; fill: $gray-darkest; - position: absolute; - left: 0; - top: 2px; + margin-top: 2px; } } @@ -250,7 +270,7 @@ ul.notes { &::after { content: ''; width: 100%; - height: 67px; + height: 70px; position: absolute; left: 0; bottom: 0; @@ -453,7 +473,7 @@ ul.notes { } .more-actions { - display: inline; + display: inline-block; .tooltip { white-space: nowrap; @@ -509,11 +529,6 @@ ul.notes { display: inline; line-height: 20px; - @include notes-media('min', $screen-sm-min) { - margin-left: 10px; - line-height: 24px; - } - .fa { color: $gray-darkest; position: relative; @@ -644,15 +659,12 @@ ul.notes { .discussion-body, .diff-file { .notes .note { - padding-left: $gl-padding; - padding-right: $gl-padding; - - &.system-note { - padding-left: 0; + border-bottom: 1px solid $white-normal; - @media (min-width: $screen-sm-min) { - margin-left: 70px; - } + .timeline-entry-inner { + padding-left: $gl-padding; + padding-right: $gl-padding; + border-bottom: none; } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 3d88e273a9e..9637d26e56d 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -10,17 +10,13 @@ .table-holder { width: 100%; - - @media (max-width: $screen-sm-max) { - overflow: auto; - } } .commit-title { margin: 0; } - .table.ci-table { + .ci-table { .label { margin-bottom: 3px; @@ -30,11 +26,6 @@ color: $black; } - .stage-cell { - min-width: 130px; // Guarantees we show at least 4 stages in line - width: 20%; - } - .pipelines-time-ago { text-align: right; } @@ -108,39 +99,7 @@ } } -.table.ci-table { - - &.builds-page tbody tr { - height: 71px; - } - - tr { - th { - padding: 16px 8px; - border: none; - } - - td { - padding: 10px 8px; - } - - td.environments-actions { - padding-right: 0; - } - - td.stage-cell { - padding: 10px 0; - } - - .commit-link { - padding: 9px 8px 10px 2px; - } - } - - tbody { - border-top-width: 1px; - } - +.ci-table { .build.retried { background-color: $gray-lightest; } @@ -174,7 +133,7 @@ overflow: hidden; display: inline-block; white-space: nowrap; - vertical-align: top; + vertical-align: middle; text-overflow: ellipsis; } @@ -194,13 +153,6 @@ color: $gl-link-color; } - .commit-title { - max-width: 225px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .label { margin-right: 4px; } @@ -253,11 +205,7 @@ } .stage-cell { - font-size: 0; - padding: 0 4px; - - > .stage-container > div > button > span > svg, - > .stage-container > button > svg { + .mini-pipeline-graph-dropdown-toggle svg { height: 22px; width: 22px; position: absolute; @@ -631,6 +579,23 @@ font-weight: normal; } +@mixin mini-pipeline-graph-color($color-light, $color-main, $color-dark) { + border-color: $color-main; + color: $color-main; + + &:hover, + &:focus, + &:active { + background-color: $color-light; + border-color: $color-dark; + color: $color-dark; + + svg { + fill: $color-dark; + } + } +} + // Dropdown button in mini pipeline graph .mini-pipeline-graph-dropdown-toggle { border-radius: 100px; @@ -670,100 +635,32 @@ // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - border-color: $green-500; - color: $green-500; - - &:hover, - &:focus, - &:active { - background-color: $green-50; - border-color: $green-600; - color: $green-600; - - svg { - fill: $green-600; - } - } + @include mini-pipeline-graph-color($green-50, $green-500, $green-600); } &.ci-status-icon-failed { - border-color: $red-500; - color: $red-500; - - &:hover, - &:focus, - &:active { - background-color: $red-50; - border-color: $red-600; - color: $red-600; - - svg { - fill: $red-600; - } - } + @include mini-pipeline-graph-color($red-50, $red-500, $red-600); } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - border-color: $orange-500; - color: $orange-500; - - &:hover, - &:focus, - &:active { - background-color: $orange-50; - border-color: $orange-600; - color: $orange-600; - - svg { - fill: $orange-600; - } - } + @include mini-pipeline-graph-color($orange-50, $orange-500, $orange-600); } &.ci-status-icon-running { - border-color: $blue-400; - color: $blue-400; - - &:hover, - &:focus, - &:active { - background-color: $blue-50; - border-color: $blue-600; - color: $blue-600; - - svg { - fill: $blue-600; - } - } + @include mini-pipeline-graph-color($blue-50, $blue-400, $blue-600); } &.ci-status-icon-canceled, &.ci-status-icon-disabled, &.ci-status-icon-not-found, &.ci-status-icon-manual { - border-color: $gl-text-color; - color: $gl-text-color; - - &:hover, - &:focus, - &:active { - background-color: rgba($gl-text-color, 0.1); - border-color: $gl-text-color; - } + @include mini-pipeline-graph-color(rgba($gl-text-color, 0.1), $gl-text-color, $gl-text-color); } &.ci-status-icon-created, &.ci-status-icon-skipped { - border-color: $gray-darkest; - color: $gray-darkest; - - &:hover, - &:focus, - &:active { - background-color: rgba($gray-darkest, 0.1); - border-color: $gray-darkest; - } + @include mini-pipeline-graph-color(rgba($gray-darkest, 0.1), $gray-darkest, $gray-darkest); } } @@ -842,6 +739,10 @@ top: 1px; vertical-align: text-bottom; position: relative; + + @media (max-width: $screen-xs-max) { + max-width: 60%; + } } // status icon on the left @@ -932,6 +833,11 @@ left: 50%; transform: translate(-50%, 0); border-width: 0 5px 6px; + + @media (max-width: $screen-sm-max) { + left: 100%; + margin-left: -12px; + } } &::before { @@ -949,9 +855,15 @@ * Center dropdown menu in mini graph */ .mini-pipeline-graph-dropdown-menu.dropdown-menu { - right: auto; - left: 50%; - transform: translate(-50%, 0); + transform: translate(-80%, 0); + min-width: 150px; + + @media(min-width: $screen-md-min) { + transform: translate(-50%, 0); + right: auto; + left: 50%; + min-width: 240px; + } } /** * Terminal diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 062665bc634..562ecbc6986 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -380,7 +380,7 @@ a.deploy-project-label { padding: 0; background: transparent; border: none; - line-height: 36px; + line-height: 34px; margin: 0; > li + li::before { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 33b3c083fd2..d69a8e0995c 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -29,6 +29,10 @@ &:first-of-type { margin-top: 10px; } + + &.expanded { + overflow: visible; + } } .settings-header { @@ -122,3 +126,66 @@ margin-left: 5px; } } + +.prometheus-metrics-monitoring { + .panel { + .panel-toggle { + width: 14px; + } + + .badge { + font-size: inherit; + } + + .panel-heading .badge-count { + color: $white-light; + background: $common-gray-dark; + } + + .panel-body { + padding: 0; + } + + .flash-container { + margin-bottom: 0; + cursor: default; + + .flash-notice { + border-radius: 0; + } + } + } + + .loading-metrics, + .empty-metrics { + padding: 30px 10px; + + p, + .btn { + margin-top: 10px; + margin-bottom: 0; + } + } + + .loading-metrics .metrics-load-spinner { + color: $loading-color; + } + + .metrics-list { + margin-bottom: 0; + + li { + padding: $gl-padding; + + .badge { + margin-left: 5px; + background: $badge-bg; + } + } + + /* Ensure we don't add border if there's only single li */ + li + li { + border-top: 1px solid $border-color; + } + } +} diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 4ed8617b6a3..67ad1ae60af 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,142 +1,82 @@ -.container-fluid { - .ci-status { - padding: 2px 7px 4px; - margin-right: 10px; - border: 1px solid $gray-darker; - white-space: nowrap; - border-radius: 4px; - - &:hover, - &:focus { - text-decoration: none; - } - - svg { - height: 13px; - width: 13px; - position: relative; - top: 2px; - overflow: visible; - } +@mixin status-color($color-light, $color-main, $color-dark) { + color: $color-main; + border-color: $color-main; - &.ci-failed { - color: $red-500; - border-color: $red-500; + &:not(span):hover { + background-color: $color-light; + color: $color-dark; + border-color: $color-dark; - &:not(span):hover { - background-color: $red-50; - color: $red-600; - border-color: $red-600; - - svg { - fill: $red-600; - } - } - - svg { - fill: $red-500; - } + svg { + fill: $color-dark; } + } - &.ci-success { - color: $green-600; - border-color: $green-500; + svg { + fill: $color-main; + } +} - &:not(span):hover { - background-color: $green-50; - color: $green-700; - border-color: $green-600; +.ci-status { + padding: 2px 7px 4px; + border: 1px solid $gray-darker; + white-space: nowrap; + border-radius: 4px; - svg { - fill: $green-600; - } - } + &:hover, + &:focus { + text-decoration: none; + } - svg { - fill: $green-500; - } - } + svg { + height: 13px; + width: 13px; + position: relative; + top: 2px; + overflow: visible; + } - &.ci-canceled, - &.ci-disabled { - color: $gl-text-color; - border-color: $gl-text-color; + &.ci-failed { + @include status-color($red-50, $red-500, $red-600); + } - &:not(span):hover { - background-color: rgba($gl-text-color, .07); - } + &.ci-success { + @include status-color($green-50, $green-500, $green-700); + } - svg { - fill: $gl-text-color; - } - } + &.ci-canceled, + &.ci-disabled, + &.ci-manual { + color: $gl-text-color; + border-color: $gl-text-color; - &.ci-pending, - &.ci-success_with_warnings, - &.ci-failed_with_warnings { - color: $orange-600; - border-color: $orange-500; - - &:not(span):hover { - background-color: $orange-50; - color: $orange-700; - border-color: $orange-600; - - svg { - fill: $orange-600; - } - } - - svg { - fill: $orange-500; - } + &:not(span):hover { + background-color: rgba($gl-text-color, .07); } + } - &.ci-info, - &.ci-running { - color: $blue-500; - border-color: $blue-500; - - &:not(span):hover { - background-color: $blue-50; - color: $blue-600; - border-color: $blue-600; - - svg { - fill: $blue-600; - } - } - - svg { - fill: $blue-500; - } - } + &.ci-pending, + &.ci-failed_with_warnings, + &.ci-success_with_warnings { + @include status-color($orange-50, $orange-500, $orange-700); + } - &.ci-created, - &.ci-skipped { - color: $gl-text-color-secondary; - border-color: $gl-text-color-secondary; + &.ci-info, + &.ci-running { + @include status-color($blue-50, $blue-500, $blue-600); + } - &:not(span):hover { - background-color: rgba($gl-text-color-secondary, .07); - } + &.ci-created, + &.ci-skipped { + color: $gl-text-color-secondary; + border-color: $gl-text-color-secondary; - svg { - fill: $gl-text-color-secondary; - } + &:not(span):hover { + background-color: rgba($gl-text-color-secondary, .07); } - &.ci-manual { - color: $gl-text-color; - border-color: $gl-text-color; - - &:not(span):hover { - background-color: rgba($gl-text-color, .07); - } - - svg { - fill: $gl-text-color; - } + svg { + fill: $gl-text-color-secondary; } } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index ab63225147f..ce1a13c6afa 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,6 +1,72 @@ .tree-holder { - > .nav-block { - margin: 11px 0; + .nav-block { + margin: 10px 0; + + @media (min-width: $screen-sm-min) { + display: flex; + + .tree-ref-container { + flex: 1; + } + + .tree-controls { + text-align: right; + + .btn-group { + margin-left: 10px; + } + } + + .tree-ref-holder { + float: left; + margin-right: 15px; + } + + .repo-breadcrumb { + li:last-of-type { + position: relative; + } + } + + .add-to-tree-dropdown { + position: absolute; + left: 18px; + } + } + } + + @media (max-width: $screen-xs-max) { + .repo-breadcrumb { + margin-top: 10px; + position: relative; + + .dropdown-menu { + min-width: 100%; + width: 100%; + left: inherit; + right: 0; + } + } + + .add-to-tree-dropdown { + position: absolute; + left: 0; + right: 0; + } + + .tree-controls { + margin-bottom: 10px; + + .btn, + .dropdown, + .btn-group { + width: 100%; + } + + .btn { + margin: 10px 0 0; + } + } } .file-finder { @@ -131,11 +197,6 @@ } } -.tree-ref-holder { - float: left; - margin-right: 15px; -} - .blob-commit-info { list-style: none; margin: 0; @@ -159,16 +220,6 @@ color: $md-link-color; } -.tree-controls { - float: right; - position: relative; - z-index: 2; - - .project-action-button { - margin-left: $btn-side-margin; - } -} - .repo-charts { .sub-header { margin: 20px 0; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 75fb19e815f..4d4b8a8425f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -100,6 +100,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :enabled_git_access_protocol, :gravatar_enabled, :help_page_text, + :help_page_hide_commercial_content, + :help_page_support_url, :home_page_url, :housekeeping_bitmaps_enabled, :housekeeping_enabled, diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b09eef17c23..fa1bc72560e 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -54,7 +54,7 @@ class Admin::UsersController < Admin::ApplicationController end def block - if user.block + if update_user { |user| user.block } redirect_back_or_admin_user(notice: "Successfully blocked") else redirect_back_or_admin_user(alert: "Error occurred. User was not blocked") @@ -64,7 +64,7 @@ class Admin::UsersController < Admin::ApplicationController def unblock if user.ldap_blocked? redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab") - elsif user.activate + elsif update_user { |user| user.activate } redirect_back_or_admin_user(notice: "Successfully unblocked") else redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked") @@ -72,7 +72,7 @@ class Admin::UsersController < Admin::ApplicationController end def unlock - if user.unlock_access! + if update_user { |user| user.unlock_access! } redirect_back_or_admin_user(alert: "Successfully unlocked") else redirect_back_or_admin_user(alert: "Error occurred. User was not unlocked") @@ -80,7 +80,7 @@ class Admin::UsersController < Admin::ApplicationController end def confirm - if user.confirm + if update_user { |user| user.confirm } redirect_back_or_admin_user(notice: "Successfully confirmed") else redirect_back_or_admin_user(alert: "Error occurred. User was not confirmed") @@ -88,7 +88,8 @@ class Admin::UsersController < Admin::ApplicationController end def disable_two_factor - user.disable_two_factor! + update_user { |user| user.disable_two_factor! } + redirect_to admin_user_path(user), notice: 'Two-factor Authentication has been disabled for this user' end @@ -124,15 +125,18 @@ class Admin::UsersController < Admin::ApplicationController end respond_to do |format| - user.skip_reconfirmation! - if user.update_attributes(user_params_with_pass) + result = Users::UpdateService.new(user, user_params_with_pass).execute do |user| + user.skip_reconfirmation! + end + + if result[:status] == :success format.html { redirect_to [:admin, user], notice: 'User was successfully updated.' } format.json { head :ok } else # restore username to keep form action url. user.username = params[:id] format.html { render "edit" } - format.json { render json: user.errors, status: :unprocessable_entity } + format.json { render json: [result[:message]], status: result[:status] } end end end @@ -148,13 +152,16 @@ class Admin::UsersController < Admin::ApplicationController def remove_email email = user.emails.find(params[:email_id]) - email.destroy - - user.update_secondary_emails! + success = Emails::DestroyService.new(user, email: email.email).execute respond_to do |format| - format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } - format.js { head :ok } + if success + format.html { redirect_back_or_admin_user(notice: 'Successfully removed email.') } + format.json { head :ok } + else + format.html { redirect_back_or_admin_user(alert: 'There was an error removing the e-mail.') } + format.json { render json: 'There was an error removing the e-mail.', status: 400 } + end end end @@ -202,4 +209,10 @@ class Admin::UsersController < Admin::ApplicationController :website_url ] end + + def update_user(&block) + result = Users::UpdateService.new(user).execute(&block) + + result[:status] == :success + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 91694ebcd1d..824ce845706 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base render_404 end + rescue_from(ActionController::UnknownFormat) do + render_404 + end + rescue_from Gitlab::Access::AccessDeniedError do |exception| render_403 end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 36ad307a93b..1a9904bbe57 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -97,8 +97,8 @@ module CreatesCommit def merge_request_exists? return @merge_request if defined?(@merge_request) - @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. - find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened + .find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch) end def different_project? diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 8d07780f6c2..47d9ae350ae 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -15,8 +15,8 @@ module MembershipActions end def destroy - Members::DestroyService.new(membershipable, current_user, params). - execute(:all) + Members::DestroyService.new(membershipable, current_user, params) + .execute(:all) respond_to do |format| format.html do @@ -42,8 +42,8 @@ module MembershipActions end def leave - member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id). - execute(:all) + member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id) + .execute(:all) notice = if member.request? diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index b2536a1c949..1ff785ac2ca 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -6,7 +6,7 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_merge_requests_tab", { - merge_requests: @milestone.merge_requests, + merge_requests: @milestone.sorted_merge_requests, show_project_name: true }) end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 641c502dbe4..91c1e4dff79 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -22,8 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def starred - @projects = load_projects(params.merge(starred: true)). - includes(:forked_from_project, :tags).page(params[:page]) + @projects = load_projects(params.merge(starred: true)) + .includes(:forked_from_project, :tags).page(params[:page]) @groups = [] @@ -45,8 +45,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def load_projects(finder_params) - ProjectsFinder.new(params: finder_params, current_user: current_user). - execute.includes(:route, namespace: :route) + ProjectsFinder.new(params: finder_params, current_user: current_user) + .execute.includes(:route, namespace: :route) end def load_events diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 8f1870759e4..741879dee35 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -49,7 +49,7 @@ class Explore::ProjectsController < Explore::ApplicationController private def load_projects - ProjectsFinder.new(current_user: current_user, params: params). - execute.includes(:route, namespace: :route) + ProjectsFinder.new(current_user: current_user, params: params) + .execute.includes(:route, namespace: :route) end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 11db164b3fa..4bceb1d67a3 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -11,8 +11,8 @@ class JwtController < ApplicationController service = SERVICES[params[:service]] return head :not_found unless service - result = service.new(@authentication_result.project, @authentication_result.actor, auth_params). - execute(authentication_abilities: @authentication_result.authentication_abilities) + result = service.new(@authentication_result.project, @authentication_result.actor, auth_params) + .execute(authentication_abilities: @authentication_result.authentication_abilities) render json: result, status: result[:http_status] end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 2a8c8ca4bad..b82681b197e 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -144,7 +144,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end def log_audit_event(user, options = {}) - AuditEventService.new(user, user, options). - for_authentication.security_event + AuditEventService.new(user, user, options) + .for_authentication.security_event end end diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb index 933e0f3bceb..408650aac54 100644 --- a/app/controllers/profiles/avatars_controller.rb +++ b/app/controllers/profiles/avatars_controller.rb @@ -1,9 +1,8 @@ class Profiles::AvatarsController < Profiles::ApplicationController def destroy @user = current_user - @user.remove_avatar! - @user.save + Users::UpdateService.new(@user).execute { |user| user.remove_avatar! } redirect_to profile_path, status: 302 end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 5655fb2ba0e..17b66df43e7 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -5,9 +5,9 @@ class Profiles::EmailsController < Profiles::ApplicationController end def create - @email = current_user.emails.new(email_params) + @email = Emails::CreateService.new(current_user, email_params).execute - if @email.save + if @email.errors.blank? NotificationService.new.new_email(@email) else flash[:alert] = @email.errors.full_messages.first @@ -18,9 +18,8 @@ class Profiles::EmailsController < Profiles::ApplicationController def destroy @email = current_user.emails.find(params[:id]) - @email.destroy - current_user.update_secondary_emails! + Emails::DestroyService.new(current_user, email: @email.email).execute respond_to do |format| format.html { redirect_to profile_emails_url, status: 302 } diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index a271e2dfc4b..960b7512602 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -7,7 +7,9 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def update - if current_user.update_attributes(user_params) + result = Users::UpdateService.new(current_user, user_params).execute + + if result[:status] == :success flash[:notice] = "Notification settings saved" else flash[:alert] = "Failed to save new settings" diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index 6217ec5ecef..10145bae0d3 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -15,17 +15,17 @@ class Profiles::PasswordsController < Profiles::ApplicationController return end - new_password = user_params[:password] - new_password_confirmation = user_params[:password_confirmation] - - result = @user.update_attributes( - password: new_password, - password_confirmation: new_password_confirmation, + password_attributes = { + password: user_params[:password], + password_confirmation: user_params[:password_confirmation], password_automatically_set: false - ) + } + + result = Users::UpdateService.new(@user, password_attributes).execute + + if result[:status] == :success + Users::UpdateService.new(@user, password_expires_at: nil).execute - if result - @user.update_attributes(password_expires_at: nil) redirect_to root_path, notice: 'Password successfully changed' else render :new @@ -46,7 +46,9 @@ class Profiles::PasswordsController < Profiles::ApplicationController return end - if @user.update_attributes(password_attributes) + result = Users::UpdateService.new(@user, password_attributes).execute + + if result[:status] == :success flash[:notice] = "Password was successfully updated. Please login with it" redirect_to new_user_session_path else diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 5414142e2df..1e557c47638 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -6,7 +6,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController def update begin - if @user.update_attributes(preferences_params) + result = Users::UpdateService.new(user, preferences_params).execute + + if result[:status] == :success flash[:notice] = 'Preferences saved.' else flash[:alert] = 'Failed to save preferences.' diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 313cdcd1c15..1a4f77639e7 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController current_user.otp_grace_period_started_at = Time.current end - current_user.save! if current_user.changed? + Users::UpdateService.new(current_user).execute! if two_factor_authentication_required? && !current_user.two_factor_enabled? two_factor_authentication_reason( @@ -41,9 +41,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def create if current_user.validate_and_consume_otp!(params[:pin_code]) - current_user.otp_required_for_login = true - @codes = current_user.generate_otp_backup_codes! - current_user.save! + Users::UpdateService.new(current_user, otp_required_for_login: true).execute! do |user| + @codes = user.generate_otp_backup_codes! + end render 'create' else @@ -70,8 +70,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end def codes - @codes = current_user.generate_otp_backup_codes! - current_user.save! + Users::UpdateService.new(current_user).execute! do |user| + @codes = user.generate_otp_backup_codes! + end end def destroy diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 72f34930ca8..076076fd1b3 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -12,55 +12,64 @@ class ProfilesController < Profiles::ApplicationController user_params.except!(:email) if @user.external_email? respond_to do |format| - if @user.update_attributes(user_params) + result = Users::UpdateService.new(@user, user_params).execute + + if result[:status] == :success message = "Profile was successfully updated" + format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) } format.json { render json: { message: message } } else - message = @user.errors.full_messages.uniq.join('. ') - format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) } - format.json { render json: { message: message }, status: :unprocessable_entity } + format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: result[:message] }) } + format.json { render json: result } end end end def reset_private_token - if current_user.reset_authentication_token! - flash[:notice] = "Private token was successfully reset" + Users::UpdateService.new(@user).execute! do |user| + user.reset_authentication_token! end + flash[:notice] = "Private token was successfully reset" + redirect_to profile_account_path end def reset_incoming_email_token - if current_user.reset_incoming_email_token! - flash[:notice] = "Incoming email token was successfully reset" + Users::UpdateService.new(@user).execute! do |user| + user.reset_incoming_email_token! end + flash[:notice] = "Incoming email token was successfully reset" + redirect_to profile_account_path end def reset_rss_token - if current_user.reset_rss_token! - flash[:notice] = "RSS token was successfully reset" + Users::UpdateService.new(@user).execute! do |user| + user.reset_rss_token! end + flash[:notice] = "RSS token was successfully reset" + redirect_to profile_account_path end def audit_log - @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id). - order("created_at DESC"). - page(params[:page]) + @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id) + .order("created_at DESC") + .page(params[:page]) end def update_username - if @user.update_attributes(username: user_params[:username]) - options = { notice: "Username successfully changed" } - else - message = @user.errors.full_messages.uniq.join('. ') - options = { alert: "Username change failed - #{message}" } - end + result = Users::UpdateService.new(@user, username: user_params[:username]).execute + + options = if result[:status] == :success + { notice: "Username successfully changed" } + else + { alert: "Username change failed - #{result[:message]}" } + end redirect_back_or_default(default: { action: 'show' }, options: options) end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index cb4bd0ad5f5..3d7ce4f0222 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -53,9 +53,21 @@ class Projects::ApplicationController < ApplicationController end end + def check_project_feature_available!(feature) + render_404 unless project.feature_available?(feature, current_user) + end + + def check_issuables_available! + render_404 unless project.feature_available?(:issues, current_user) || + project.feature_available?(:merge_requests, current_user) + end + def method_missing(method_sym, *arguments, &block) - if method_sym.to_s =~ /\Aauthorize_(.*)!\z/ + case method_sym.to_s + when /\Aauthorize_(.*)!\z/ authorize_action!($1.to_sym) + when /\Acheck_(.*)_available!\z/ + check_project_feature_available!($1.to_sym) else super end @@ -80,10 +92,6 @@ class Projects::ApplicationController < ApplicationController cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? end - def builds_enabled - return render_404 unless @project.feature_available?(:builds, current_user) - end - def require_pages_enabled! not_found unless Gitlab.config.pages.enabled end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 66e6a9a451c..a82d6fd5a4a 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -187,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController end def set_last_commit_sha - @last_commit_sha = Gitlab::Git::Commit. - last_for_path(@repository, @ref, @path).sha + @last_commit_sha = Gitlab::Git::Commit + .last_for_path(@repository, @ref, @path).sha end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 70b06cfd9b4..94a752c21eb 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -37,8 +37,8 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present? - result = CreateBranchService.new(project, current_user). - execute(branch_name, ref) + result = CreateBranchService.new(project, current_user) + .execute(branch_name, ref) if params[:issue_iid] issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid]) diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index f33797ca310..37b5a6e9d48 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -18,11 +18,11 @@ class Projects::CommitsController < Projects::ApplicationController @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end - @note_counts = project.notes.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + @note_counts = project.notes.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count - @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. - find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened + .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) respond_to do |format| format.html diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 88dd600e5fe..ef400c4d745 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -61,7 +61,7 @@ class Projects::CompareController < Projects::ApplicationController end def merge_request - @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. - find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened + .find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 7f1469e107d..c2e621fa190 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -6,7 +6,7 @@ class Projects::DeployKeysController < Projects::ApplicationController before_action :authorize_admin_project! before_action :authorize_update_deploy_key!, only: [:edit, :update] - layout "project_settings" + layout 'project_settings' def index respond_to do |format| @@ -66,7 +66,7 @@ class Projects::DeployKeysController < Projects::ApplicationController protected def deploy_key - @deploy_key ||= @project.deploy_keys.find(params[:id]) + @deploy_key ||= DeployKey.find(params[:id]) end def create_params diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index 6644deb49c9..47c312ffddf 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -22,6 +22,22 @@ class Projects::DeploymentsController < Projects::ApplicationController render_404 end + def additional_metrics + return render_404 unless deployment.has_additional_metrics? + + respond_to do |format| + format.json do + metrics = deployment.additional_metrics + + if metrics.any? + render json: metrics + else + head :no_content + end + end + end + end + private def deployment diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index f4a18a5e8f7..2e6ab7903b8 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -1,5 +1,5 @@ class Projects::DiscussionsController < Projects::ApplicationController - before_action :module_enabled + before_action :check_merge_requests_available! before_action :merge_request before_action :discussion before_action :authorize_resolve_discussion! @@ -34,8 +34,4 @@ class Projects::DiscussionsController < Projects::ApplicationController def authorize_resolve_discussion! access_denied! unless discussion.can_resolve?(current_user) end - - def module_enabled - render_404 unless @project.feature_available?(:merge_requests, current_user) - end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 4630f451445..f88a1ffd1e9 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,8 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController respond_to do |format| format.html format.json do - Gitlab::PollingInterval.set_header(response, interval: 3_000) - render json: { environments: EnvironmentSerializer .new(project: @project, current_user: @current_user) @@ -131,6 +129,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end + def additional_metrics + respond_to do |format| + format.json do + additional_metrics = environment.additional_metrics || {} + + render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content + end + end + end + private def verify_api_request! diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 928f17e6a8e..7d0e2b3e2ef 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController include ActionController::HttpAuthentication::Basic include KerberosSpnegoHelper - attr_reader :authentication_result + attr_reader :authentication_result, :redirected_path delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true @@ -14,7 +14,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController skip_before_action :verify_authenticity_token skip_before_action :repository before_action :authenticate_user - before_action :ensure_project_found! private @@ -68,38 +67,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? end - def ensure_project_found! - render_not_found if project.blank? - end - def project - return @project if defined?(@project) - - project_id, _ = project_id_with_suffix - @project = - if project_id.blank? - nil - else - Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") - end - end + parse_repo_path unless defined?(@project) - # This method returns two values so that we can parse - # params[:project_id] (untrusted input!) in exactly one place. - def project_id_with_suffix - id = params[:project_id] || '' - - %w[.wiki.git .git].each do |suffix| - if id.end_with?(suffix) - # Be careful to only remove the suffix from the end of 'id'. - # Accidentally removing it from the middle is how security - # vulnerabilities happen! - return [id.slice(0, id.length - suffix.length), suffix] - end - end + @project + end - # Something is wrong with params[:project_id]; do not pass it on. - [nil, nil] + def parse_repo_path + @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") end def render_missing_personal_token @@ -114,14 +89,9 @@ class Projects::GitHttpClientController < Projects::ApplicationController end def wiki? - return @wiki if defined?(@wiki) - - _, suffix = project_id_with_suffix - @wiki = suffix == '.wiki.git' - end + parse_repo_path unless defined?(@wiki) - def render_not_found - render plain: 'Not Found', status: :not_found + @wiki end def handle_basic_authentication(login, password) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index b6b62da7b60..71ae60cb8cd 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -56,7 +56,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def access - @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities) + @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path) end def access_actor diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 43fc0c39801..df5221fe95f 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -5,7 +5,6 @@ class Projects::GraphsController < Projects::ApplicationController before_action :require_non_empty_project before_action :assign_ref_vars before_action :authorize_download_code! - before_action :builds_enabled, only: :ci def show respond_to do |format| diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 56f76e752d0..dfc6baa34a4 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -9,7 +9,7 @@ class Projects::IssuesController < Projects::ApplicationController prepend_before_action :authenticate_user!, only: [:new] before_action :redirect_to_external_issue_tracker, only: [:index, :new] - before_action :module_enabled + before_action :check_issues_available! before_action :issue, except: [:index, :new, :create, :bulk_update] # Allow write(create) issue @@ -250,7 +250,7 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) end - def module_enabled + def check_issues_available! return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 1beac202efe..daa973c9281 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,7 +1,7 @@ class Projects::LabelsController < Projects::ApplicationController include ToggleSubscriptionAction - before_action :module_enabled + before_action :check_issuables_available! 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! @@ -135,12 +135,6 @@ class Projects::LabelsController < Projects::ApplicationController protected - def module_enabled - unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) - return render_404 - end - end - def label_params params.require(:label).permit(:title, :description, :color) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 314906b5f09..879ff6d393e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,7 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include ToggleAwardEmoji include IssuableCollections - before_action :module_enabled + before_action :check_merge_requests_available! before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :commit_change_content @@ -143,8 +143,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Get commits from repository # or from cache if already merged @commits = @merge_request.commits - @note_counts = Note.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + @note_counts = Note.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } end @@ -192,9 +192,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end begin - MergeRequests::Conflicts::ResolveService. - new(merge_request). - execute(current_user, params) + MergeRequests::Conflicts::ResolveService + .new(merge_request) + .execute(current_user, params) flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' @@ -461,10 +461,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) end - def module_enabled - return render_404 unless @project.feature_available?(:merge_requests, current_user) - end - def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch @@ -562,8 +558,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.compare_commits.reverse @commit = @merge_request.diff_head_commit - @note_counts = Note.where(commit_id: @commits.map(&:id)). - group(:commit_id).count + @note_counts = Note.where(commit_id: @commits.map(&:id)) + .group(:commit_id).count @labels = LabelsFinder.new(current_user, project_id: @project.id).execute @@ -579,10 +575,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_request_params params.require(:merge_request) - .permit(merge_request_params_ce) + .permit(merge_request_params_attributes) end - def merge_request_params_ce + def merge_request_params_attributes [ :assignee_id, :description, @@ -602,7 +598,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_params - params.permit(:should_remove_source_branch, :commit_message) + params.permit(merge_params_attributes) + end + + def merge_params_attributes + [:should_remove_source_branch, :commit_message] end # Make sure merge requests created before 8.0 diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index ae16f69955a..953b1e83e49 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,8 +1,8 @@ class Projects::MilestonesController < Projects::ApplicationController include MilestoneActions - before_action :module_enabled - before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests, :merge_requests, :participants, :labels] + before_action :check_issuables_available! + before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels] # Allow read any milestone before_action :authorize_read_milestone! @@ -85,22 +85,6 @@ class Projects::MilestonesController < Projects::ApplicationController end end - def sort_issues - @milestone.sort_issues(params['sortable_issue'].map(&:to_i)) - - render json: { saved: true } - end - - def sort_merge_requests - @merge_requests = @milestone.merge_requests.where(id: params['sortable_merge_request']) - @merge_requests.each do |merge_request| - merge_request.position = params['sortable_merge_request'].index(merge_request.id.to_s) + 1 - merge_request.save - end - - render json: { saved: true } - end - protected def milestone @@ -111,12 +95,6 @@ class Projects::MilestonesController < Projects::ApplicationController return render_404 unless can?(current_user, :admin_milestone, @project) end - def module_enabled - unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) - return render_404 - end - end - def milestone_params params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 6223e7943f8..303e91a8dc0 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -4,7 +4,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] - before_action :builds_enabled, only: :charts wrap_parameters Ci::Pipeline @@ -136,7 +135,12 @@ class Projects::PipelinesController < Projects::ApplicationController @charts[:week] = Ci::Charts::WeekChart.new(project) @charts[:month] = Ci::Charts::MonthChart.new(project) @charts[:year] = Ci::Charts::YearChart.new(project) - @charts[:build_times] = Ci::Charts::BuildTime.new(project) + @charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project) + + @counts = {} + @counts[:total] = @project.pipelines.count(:all) + @counts[:success] = @project.pipelines.success.count(:all) + @counts[:failed] = @project.pipelines.failed.count(:all) end private diff --git a/app/controllers/projects/prometheus_controller.rb b/app/controllers/projects/prometheus_controller.rb new file mode 100644 index 00000000000..507468d7102 --- /dev/null +++ b/app/controllers/projects/prometheus_controller.rb @@ -0,0 +1,24 @@ +class Projects::PrometheusController < Projects::ApplicationController + before_action :authorize_read_project! + before_action :require_prometheus_metrics! + + def active_metrics + respond_to do |format| + format.json do + matched_metrics = project.prometheus_service.matched_metrics || {} + + if matched_metrics.any? + render json: matched_metrics + else + head :no_content + end + end + end + end + + private + + def require_prometheus_metrics! + render_404 unless project.prometheus_service.present? + end +end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 6f009d61950..24fe78bc1bd 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -14,8 +14,8 @@ module Projects def define_runners_variables @project_runners = @project.runners.ordered - @assignable_runners = current_user.ci_authorized_runners. - assignable_for(project).ordered.page(params[:page]).per(20) + @assignable_runners = current_user.ci_authorized_runners + .assignable_for(project).ordered.page(params[:page]).per(20) @shared_runners = Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 8a8f8d6a27d..98dd307bd9d 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -5,7 +5,7 @@ class Projects::SnippetsController < Projects::ApplicationController include SnippetsActions include RendersBlob - before_action :module_enabled + before_action :check_snippets_available! before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] # Allow read any snippet @@ -102,10 +102,6 @@ class Projects::SnippetsController < Projects::ApplicationController return render_404 unless can?(current_user, :admin_project_snippet, @snippet) end - def module_enabled - return render_404 unless @project.feature_available?(:snippets, current_user) - end - def snippet_params params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description) end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index afbea3e2b40..ebc9f4edab4 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -29,8 +29,8 @@ class Projects::TagsController < Projects::ApplicationController end def create - result = Tags::CreateService.new(@project, current_user). - execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) + result = Tags::CreateService.new(@project, current_user) + .execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) if result[:status] == :success @tag = result[:tag] diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d7c702b94f8..f39441a281e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -60,10 +60,11 @@ class SessionsController < Devise::SessionsController return unless user && user.require_password? - token = user.generate_reset_token - user.save + Users::UpdateService.new(user).execute do |user| + @token = user.generate_reset_token + end - redirect_to edit_user_password_path(reset_password_token: token), + redirect_to edit_user_password_path(reset_password_token: @token), notice: "Please create a password for your new account." end @@ -128,8 +129,8 @@ class SessionsController < Devise::SessionsController end def log_audit_event(user, options = {}) - AuditEventService.new(user, user, options). - for_authentication.security_event + AuditEventService.new(user, user, options) + .for_authentication.security_event end def log_user_activity(user) diff --git a/app/controllers/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb index 682ca5e3821..6bdd3568a78 100644 --- a/app/controllers/sherlock/application_controller.rb +++ b/app/controllers/sherlock/application_controller.rb @@ -4,8 +4,8 @@ module Sherlock def find_transaction if params[:transaction_id] - @transaction = Gitlab::Sherlock.collection. - find_transaction(params[:transaction_id]) + @transaction = Gitlab::Sherlock.collection + .find_transaction(params[:transaction_id]) end end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c211106fbaa..8131eba6a2f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -106,11 +106,11 @@ class UsersController < ApplicationController def load_events # Get user activity feed for projects common for both users - @events = user.recent_events. - merge(projects_for_current_user). - references(:project). - with_associations. - limit_recent(20, params[:offset]) + @events = user.recent_events + .merge(projects_for_current_user) + .references(:project) + .with_associations + .limit_recent(20, params[:offset]) end def load_projects diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index b0450ddc1fd..46ecbaba73a 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -33,7 +33,8 @@ class EventsFinder private def by_current_user_access(events) - events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project) + events.merge(ProjectsFinder.new(current_user: current_user).execute) + .joins(:project) end def by_action(events) diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index fce3775f40e..067aff408df 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -8,9 +8,9 @@ class GroupMembersFinder return group_members unless @group.parent - parents_members = GroupMember.non_request. - where(source_id: @group.ancestors.select(:id)). - where.not(user_id: @group.users.select(:id)) + parents_members = GroupMember.non_request + .where(source_id: @group.ancestors.select(:id)) + .where.not(user_id: @group.users.select(:id)) wheres = ["members.id IN (#{group_members.select(:id).to_sql})"] wheres << "members.id IN (#{parents_members.select(:id).to_sql})" diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index f043c38c6f9..f2d3b90b8e2 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder private def init_collection - only_owned = options.fetch(:only_owned, false) - only_shared = options.fetch(:only_shared, false) + projects = if current_user + collection_with_user + else + collection_without_user + end - projects = [] + union(projects) + end - if current_user - if group.users.include?(current_user) - projects << group.projects unless only_shared - projects << group.shared_projects unless only_owned + def collection_with_user + if group.users.include?(current_user) + if only_shared? + [shared_projects] + elsif only_owned? + [owned_projects] else - unless only_shared - projects << group.projects.visible_to_user(current_user) - projects << group.projects.public_to_user(current_user) - end - - unless only_owned - projects << group.shared_projects.visible_to_user(current_user) - projects << group.shared_projects.public_to_user(current_user) - end + [shared_projects, owned_projects] end else - projects << group.projects.public_only unless only_shared - projects << group.shared_projects.public_only unless only_owned + if only_shared? + [shared_projects.public_or_visible_to_user(current_user)] + elsif only_owned? + [owned_projects.public_or_visible_to_user(current_user)] + else + [ + owned_projects.public_or_visible_to_user(current_user), + shared_projects.public_or_visible_to_user(current_user) + ] + end end + end - projects + def collection_without_user + if only_shared? + [shared_projects.public_only] + elsif only_owned? + [owned_projects.public_only] + else + [shared_projects.public_only, owned_projects.public_only] + end end def union(items) - find_union(items, Project) + if items.one? + items.first + else + find_union(items, Project) + end + end + + def only_owned? + options.fetch(:only_owned, false) + end + + def only_shared? + options.fetch(:only_shared, false) + end + + def owned_projects + group.projects + end + + def shared_projects + group.shared_projects end end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index f68610e197c..e6fb112e7f2 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -5,8 +5,10 @@ class GroupsFinder < UnionFinder end def execute - groups = find_union(all_groups, Group).with_route.order_id_desc - by_parent(groups) + items = all_groups.map do |item| + by_parent(item) + end + find_union(items, Group).with_route.order_id_desc end private @@ -16,12 +18,22 @@ class GroupsFinder < UnionFinder def all_groups groups = [] - groups << current_user.authorized_groups if current_user + if current_user + groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups + end groups << Group.unscoped.public_to_user(current_user) groups end + def groups_for_ancestors + current_user.authorized_groups + end + + def groups_for_descendants + current_user.groups + end + def by_parent(groups) return groups unless params[:parent] diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 957ad875858..558f8b5e2e5 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -41,6 +41,7 @@ class IssuableFinder items = by_iids(items) items = by_milestone(items) items = by_label(items) + items = by_created_at(items) # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far items = by_project(items) @@ -402,6 +403,18 @@ class IssuableFinder params[:non_archived].present? ? items.non_archived : items end + def by_created_at(items) + if params[:created_after].present? + items = items.where(items.klass.arel_table[:created_at].gteq(params[:created_after])) + end + + if params[:created_before].present? + items = items.where(items.klass.arel_table[:created_at].lteq(params[:created_before])) + end + + items + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index b4c074bc69c..3da5508aefd 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -41,7 +41,7 @@ class IssuesFinder < IssuableFinder def self.not_restricted_by_confidentiality(user) return Issue.where('issues.confidential IS NOT TRUE') if user.blank? - return Issue.all if user.admin? + return Issue.all if user.full_private_access? Issue.where(' issues.confidential IS NOT TRUE diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 5bf722d1ec6..8bfbe37c543 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder end def execute - items = init_collection - items = items.map do |item| - item = by_ids(item) - item = by_personal(item) - item = by_starred(item) - item = by_trending(item) - item = by_visibilty_level(item) - item = by_tags(item) - item = by_search(item) - by_archived(item) - end - items = union(items) - sort(items) + collection = init_collection + collection = by_ids(collection) + collection = by_personal(collection) + collection = by_starred(collection) + collection = by_trending(collection) + collection = by_visibilty_level(collection) + collection = by_tags(collection) + collection = by_search(collection) + collection = by_archived(collection) + + sort(collection) end private def init_collection - projects = [] + if current_user + collection_with_user + else + collection_without_user + end + end - if params[:owned].present? - projects << current_user.owned_projects if current_user + def collection_with_user + if owned_projects? + current_user.owned_projects else - projects << current_user.authorized_projects if current_user - projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present? + if private_only? + current_user.authorized_projects + else + Project.public_or_visible_to_user(current_user) + end end + end + + # Builds a collection for an anonymous user. + def collection_without_user + if private_only? || owned_projects? + Project.none + else + Project.public_to_user + end + end + + def owned_projects? + params[:owned].present? + end - projects + def private_only? + params[:non_public].present? end def by_ids(items) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 71154da7ec5..dc7ff78f3df 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -68,7 +68,7 @@ module ApplicationHelper end end - def avatar_icon(user_or_email = nil, size = nil, scale = 2) + def avatar_icon(user_or_email = nil, size = nil, scale = 2, only_path: true) user = if user_or_email.is_a?(User) user_or_email @@ -77,7 +77,7 @@ module ApplicationHelper end if user - user.avatar_url(size: size) || default_avatar + user.avatar_url(size: size, only_path: only_path) || default_avatar else gravatar_icon(user_or_email, size, scale) end @@ -167,9 +167,9 @@ module ApplicationHelper css_classes = short_format ? 'js-short-timeago' : 'js-timeago' css_classes << " #{html_class}" unless html_class.blank? - element = content_tag :time, time.strftime("%b %d, %Y"), + element = content_tag :time, l(time, format: "%b %d, %Y"), class: css_classes, - title: time.to_time.in_time_zone.to_s(:medium), + title: l(time.to_time.in_time_zone, format: :timeago_tooltip), datetime: time.to_time.getutc.iso8601, data: { toggle: 'tooltip', @@ -204,6 +204,10 @@ module ApplicationHelper 'https://' + promo_host end + def support_url + current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' + end + def page_filter_path(options = {}) without = options.delete(:without) add_label = options.delete(:label) @@ -296,4 +300,12 @@ module ApplicationHelper "https://www.twitter.com/#{name}" end end + + def can_toggle_new_nav? + Rails.env.development? + end + + def show_new_nav? + cookies["new_nav"] == "true" + end end diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb new file mode 100644 index 00000000000..d1dc4d94560 --- /dev/null +++ b/app/helpers/blame_helper.rb @@ -0,0 +1,21 @@ +module BlameHelper + def age_map_duration(blame_groups, project) + now = Time.zone.now + start_date = blame_groups.map { |blame_group| blame_group[:commit].committed_date } + .append(project.created_at).min + + { + now: now, + started_days_ago: (now - start_date).to_i / 1.day + } + end + + def age_map_class(commit_date, duration) + commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day + # Numbers 0 to 10 come from this calculation, but only commits on the oldest + # day get number 10 (all other numbers can be multiple days), so the range + # is normalized to 0-9 + age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min + "blame-commit-age-#{age_group}" + end +end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index eb03ced67eb..0a15c29cfb5 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,5 +1,5 @@ module BroadcastMessagesHelper - def broadcast_message(message = BroadcastMessage.current) + def broadcast_message(message) return unless message.present? content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 00464810054..ba84dbe4a7a 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -50,10 +50,17 @@ module ButtonHelper def http_clone_button(project, placement = 'right', append_link: true) klass = 'http-selector' - klass << ' has-tooltip' if current_user.try(:require_password?) + klass << ' has-tooltip' if current_user.try(:require_password?) || current_user.try(:require_personal_access_token?) protocol = gitlab_config.protocol.upcase + tooltip_title = + if current_user.try(:require_password?) + _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol } + else + _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol } + end + content_tag (append_link ? :a : :span), protocol, class: klass, href: (project.http_url_to_repo if append_link), @@ -61,7 +68,7 @@ module ButtonHelper html: true, placement: placement, container: 'body', - title: _("Set a password on your account to pull or push via %{protocol}") % { protocol: protocol } + title: tooltip_title } end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 5b5cdebe919..0accd1f8d77 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -85,20 +85,20 @@ module CommitsHelper if @path.blank? return link_to( - "Browse Files", + _("Browse Files"), namespace_project_tree_path(project.namespace, project, commit), class: "btn btn-default" ) elsif @repo.blob_at(commit.id, @path) return link_to( - "Browse File", + _("Browse File"), namespace_project_blob_path(project.namespace, project, tree_join(commit.id, @path)), class: "btn btn-default" ) elsif @path.present? return link_to( - "Browse Directory", + _("Browse Directory"), namespace_project_tree_path(project.namespace, project, tree_join(commit.id, @path)), class: "btn btn-default" diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 2ae3a616933..16a99addd0b 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -66,12 +66,12 @@ module DiffHelper discussions_left = discussions_right = nil - if left && (left.unchanged? || left.discussable?) + if left && left.discussable? && (left.unchanged? || left.removed?) line_code = diff_file.line_code(left) discussions_left = @grouped_diff_discussions[line_code] end - if right&.discussable? + if right && right.discussable? && right.added? line_code = diff_file.line_code(right) discussions_right = @grouped_diff_discussions[line_code] end @@ -124,6 +124,30 @@ module DiffHelper !diff_file.deleted_file? && @merge_request && @merge_request.source_project end + def diff_render_error_reason(viewer) + case viewer.render_error + when :too_large + "it is too large" + when :server_side_but_stored_externally + case viewer.diff_file.external_storage + when :lfs + 'it is stored in LFS' + else + 'it is stored externally' + end + end + end + + def diff_render_error_options(viewer) + diff_file = viewer.diff_file + options = [] + + blob_url = namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path)) + options << link_to('view the blob', blob_url) + + options + end + private def diff_btn(title, name, selected) diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 3b24f183785..fdbca789d21 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -66,4 +66,17 @@ module EmailsHelper ) end end + + def email_default_heading(text) + content_tag :h1, text, style: [ + "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif", + 'color:#333333', + 'font-size:18px', + 'font-weight:400', + 'line-height:1.4', + 'padding:0', + 'margin:0', + 'text-align:center' + ].join(';') + end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 014fc46b130..8ceb5c36bda 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -8,10 +8,10 @@ module FormHelper content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do content_tag(:h4, headline) << content_tag(:ul) do - model.errors.full_messages. - map { |msg| content_tag(:li, msg) }. - join. - html_safe + model.errors.full_messages + .map { |msg| content_tag(:li, msg) } + .join + .html_safe end end end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index c2ab80f2e0d..2e9b72e9613 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -17,13 +17,10 @@ module GraphHelper ids.zip(parent_spaces) end - def success_ratio(success_builds, failed_builds) - failed_builds = failed_builds.count(:all) - success_builds = success_builds.count(:all) + def success_ratio(counts) + return 100 if counts[:failed].zero? - return 100 if failed_builds.zero? - - ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 + ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100 ratio.to_i end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index c003b01e226..eb45241615f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -15,7 +15,7 @@ module GroupsHelper @has_group_title = true full_title = '' - group.ancestors.each do |parent| + group.ancestors.reverse.each do |parent| full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') full_title += '<span class="hidable"> / </span>'.html_safe end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5e8f0849969..3259a9c1933 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -138,8 +138,8 @@ module IssuablesHelper end output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") output end @@ -216,7 +216,8 @@ module IssuablesHelper initialTitleHtml: markdown_field(issuable, :title), initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), - initialDescriptionText: issuable.description + initialDescriptionText: issuable.description, + initialTaskStatus: issuable.task_status } data.merge!(updated_at_by(issuable)) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 4e6e6805920..6baf6f31d8f 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -133,21 +133,28 @@ module LabelsHelper end end + def can_subscribe_to_label_in_different_levels?(label) + defined?(@project) && label.is_a?(GroupLabel) + end + def label_subscription_status(label, project) - return 'project-level' if label.subscribed?(current_user, project) return 'group-level' if label.subscribed?(current_user) + return 'project-level' if label.subscribed?(current_user, project) 'unsubscribed' end - def group_label_unsubscribe_path(label, project) + def toggle_subscription_label_path(label, project) + return toggle_subscription_group_label_path(label.group, label) unless project + case label_subscription_status(label, project) - when 'project-level' then toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) when 'group-level' then toggle_subscription_group_label_path(label.group, label) + when 'project-level' then toggle_subscription_namespace_project_label_path(project.namespace, project, label) + when 'unsubscribed' then toggle_subscription_namespace_project_label_path(project.namespace, project, label) end end - def label_subscription_toggle_button_text(label, project) + def label_subscription_toggle_button_text(label, project = nil) label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe' end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index c59d8dafc83..64ad7b280cb 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -10,8 +10,8 @@ module NotesHelper Ability.can_edit_note?(current_user, note) end - def note_supports_slash_commands?(note) - Notes::SlashCommandsService.supported?(note, current_user) + def note_supports_quick_actions?(note) + Notes::QuickActionsService.supported?(note, current_user) end def noteable_json(noteable) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7441b58fddb..c04b1419a19 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,7 +80,7 @@ module ProjectsHelper end def remove_fork_project_message(project) - _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % + _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % { forked_from_project: @project.forked_from_project.name_with_namespace } end @@ -151,14 +151,21 @@ module ProjectsHelper disabled: disabled_option ) - content_tag( - :select, - options, - name: "project[project_feature_attributes][#{field}]", - id: "project_project_feature_attributes_#{field}", - class: "pull-right form-control #{repo_children_classes(field)}", - data: { field: field } - ).html_safe + content_tag :div, class: "select-wrapper" do + concat( + content_tag( + :select, + options, + name: "project[project_feature_attributes][#{field}]", + id: "project_project_feature_attributes_#{field}", + class: "pull-right form-control select-control #{repo_children_classes(field)} ", + data: { field: field } + ) + ) + concat( + icon('chevron-down') + ) + end.html_safe end def link_to_autodeploy_doc @@ -187,8 +194,25 @@ module ProjectsHelper end def load_pipeline_status(projects) - Gitlab::Cache::Ci::ProjectPipelineStatus. - load_in_batch_for_projects(projects) + Gitlab::Cache::Ci::ProjectPipelineStatus + .load_in_batch_for_projects(projects) + end + + def show_no_ssh_key_message? + cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? + end + + def show_no_password_message? + cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && + ( current_user.require_password? || current_user.require_personal_access_token? ) + end + + def link_to_set_password + if current_user.require_password? + link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path + else + link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path + end end private @@ -218,6 +242,10 @@ module ProjectsHelper nav_tabs << :container_registry end + if project.builds_enabled? && can?(current_user, :read_pipeline, project) + nav_tabs << :pipelines + end + tab_ability_map.each do |tab, ability| if can?(current_user, ability, project) nav_tabs << tab @@ -231,7 +259,6 @@ module ProjectsHelper { environments: :read_environment, milestones: :read_milestone, - pipelines: :read_pipeline, snippets: :read_project_snippet, settings: :admin_project, builds: :read_build, diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 9c46035057f..8f15904f068 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -97,8 +97,8 @@ module SearchHelper # Autocomplete results for the current user's projects def projects_autocomplete(term, limit = 5) - current_user.authorized_projects.search_by_title(term). - sorted_by_stars.non_archived.limit(limit).map do |p| + current_user.authorized_projects.search_by_title(term) + .sorted_by_stars.non_archived.limit(limit).map do |p| { category: "Projects", id: p.id, diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 9c623c9ba7c..b5f54d3e154 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -4,4 +4,14 @@ module UsersHelper title: user.email, class: 'has-tooltip commit-committer-link') end + + def user_email_help_text(user) + return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present? + + confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post + + h('Please click the link in the confirmation email before continuing. It was sent to ') + + content_tag(:strong) { user.unconfirmed_email } + h('.') + + content_tag(:p) { confirmation_link } + end end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 3e3f6246fc5..99212a3438f 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -6,8 +6,8 @@ module WikiHelper # Returns a String composed of the capitalized name of each directory and the # capitalized name of the page itself. def breadcrumb(page_slug) - page_slug.split('/'). - map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }. - join(' / ') + page_slug.split('/') + .map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize } + .join(' / ') end end diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index f7ed61625f4..962570a0efd 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -2,7 +2,9 @@ class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to - layout 'devise_mailer' + layout 'mailer/devise' + + helper EmailsHelper protected diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2192f76499d..668caef0d2c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -37,7 +37,12 @@ class ApplicationSetting < ActiveRecord::Base validates :home_page_url, allow_blank: true, url: true, - if: :home_page_url_column_exist + if: :home_page_url_column_exists? + + validates :help_page_support_url, + allow_blank: true, + url: true, + if: :help_page_support_url_column_exists? validates :after_sign_out_path, allow_blank: true, @@ -215,6 +220,7 @@ class ApplicationSetting < ActiveRecord::Base domain_whitelist: Settings.gitlab['domain_whitelist'], gravatar_enabled: Settings.gravatar['enabled'], help_page_text: nil, + help_page_hide_commercial_content: false, unique_ips_limit_per_user: 10, unique_ips_limit_time_window: 3600, unique_ips_limit_enabled: false, @@ -263,10 +269,14 @@ class ApplicationSetting < ActiveRecord::Base end end - def home_page_url_column_exist + def home_page_url_column_exists? ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) end + def help_page_support_url_column_exists? + ActiveRecord::Base.connection.column_exists?(:application_settings, :help_page_support_url) + end + def sidekiq_throttling_column_exists? ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled) end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index ebe60441603..91b62dabbcd 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -19,9 +19,9 @@ class AwardEmoji < ActiveRecord::Base class << self def votes_for_collection(ids, type) - select('name', 'awardable_id', 'COUNT(*) as count'). - where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids). - group('name', 'awardable_id') + select('name', 'awardable_id', 'COUNT(*) as count') + .where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids) + .group('name', 'awardable_id') end end diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb index e6bcacf7f70..fbc1b520c01 100644 --- a/app/models/blob_viewer/server_side.rb +++ b/app/models/blob_viewer/server_side.rb @@ -13,14 +13,12 @@ module BlobViewer end def render_error - if blob.stored_externally? - # Files that are not stored in the repository, like LFS files and - # build artifacts, can only be rendered using a client-side viewer, - # since we do not want to read large amounts of data into memory on the - # server side. Client-side viewers use JS and can fetch the file from - # `blob_raw_url` using AJAX. - return :server_side_but_stored_externally - end + # Files that are not stored in the repository, like LFS files and + # build artifacts, can only be rendered using a client-side viewer, + # since we do not want to read large amounts of data into memory on the + # server side. Client-side viewers use JS and can fetch the file from + # `blob_raw_url` using AJAX. + return :server_side_but_stored_externally if blob.stored_externally? super end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index cb40f33932a..944725d91c3 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -16,7 +16,7 @@ class BroadcastMessage < ActiveRecord::Base def self.current Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do - where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last + where('ends_at > :now AND starts_at <= :now', now: Time.zone.now).order([:created_at, :id]).to_a end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cec1ca89a6a..a300536532b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -33,7 +33,7 @@ module Ci scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } - scope :manual_actions, ->() { where(when: :manual).relevant } + scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -109,7 +109,7 @@ module Ci end def playable? - action? && manual? + action? && (manual? || complete?) end def action? @@ -138,17 +138,6 @@ module Ci ExpandVariables.expand(environment, simple_variables) if environment end - def environment_url - return @environment_url if defined?(@environment_url) - - @environment_url = - if unexpanded_url = options&.dig(:environment, :url) - ExpandVariables.expand(unexpanded_url, simple_variables) - else - persisted_environment&.external_url - end - end - def has_environment? environment.present? end @@ -192,7 +181,7 @@ module Ci slugified.gsub(/[^a-z0-9]/, '-')[0..62] end - # Variables whose value does not depend on other variables + # Variables whose value does not depend on environment def simple_variables variables = predefined_variables variables += project.predefined_variables @@ -207,7 +196,8 @@ module Ci variables end - # All variables, including those dependent on other variables + # All variables, including those dependent on environment, which could + # contain unexpanded variables. def variables simple_variables.concat(persisted_environment_variables) end @@ -481,9 +471,10 @@ module Ci variables = persisted_environment.predefined_variables - if url = environment_url - variables << { key: 'CI_ENVIRONMENT_URL', value: url, public: true } - end + # Here we're passing unexpanded environment_url for runner to expand, + # and we need to make sure that CI_ENVIRONMENT_NAME and + # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded. + variables << { key: 'CI_ENVIRONMENT_URL', value: environment_url, public: true } if environment_url variables end @@ -506,6 +497,10 @@ module Ci variables end + def environment_url + options&.dig(:environment, :url) || persisted_environment&.external_url + end + def build_attributes_from_config return {} unless pipeline.config_processor diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 9ddecba5183..1b3e5a25ac2 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -168,8 +168,8 @@ module Ci end def stages_names - statuses.order(:stage_idx).distinct. - pluck(:stage, :stage_idx).map(&:first) + statuses.order(:stage_idx).distinct + .pluck(:stage, :stage_idx).map(&:first) end def legacy_stage(name) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 487ba61bc9c..d12f96f3d0b 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -30,8 +30,8 @@ module Ci scope :assignable_for, ->(project) do # FIXME: That `to_sql` is needed to workaround a weird Rails bug. # Without that, placeholders would miss one and couldn't match. - where(locked: false). - where.not("id IN (#{project.runners.select(:id).to_sql})").specific + where(locked: false) + .where.not("id IN (#{project.runners.select(:id).to_sql})").specific end validate :tag_constraints diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 3c9c6584e02..32af5566135 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -11,18 +11,21 @@ module HasStatus class_methods do def status_sql - scope = respond_to?(:exclude_ignored) ? exclude_ignored : all - - builds = scope.select('count(*)').to_sql - created = scope.created.select('count(*)').to_sql - success = scope.success.select('count(*)').to_sql - manual = scope.manual.select('count(*)').to_sql - pending = scope.pending.select('count(*)').to_sql - running = scope.running.select('count(*)').to_sql - skipped = scope.skipped.select('count(*)').to_sql - canceled = scope.canceled.select('count(*)').to_sql + scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all + scope_warnings = respond_to?(:failed_but_allowed) ? failed_but_allowed : none + + builds = scope_relevant.select('count(*)').to_sql + created = scope_relevant.created.select('count(*)').to_sql + success = scope_relevant.success.select('count(*)').to_sql + manual = scope_relevant.manual.select('count(*)').to_sql + pending = scope_relevant.pending.select('count(*)').to_sql + running = scope_relevant.running.select('count(*)').to_sql + skipped = scope_relevant.skipped.select('count(*)').to_sql + canceled = scope_relevant.canceled.select('count(*)').to_sql + warnings = scope_warnings.select('count(*) > 0').to_sql.presence || 'false' "(CASE + WHEN (#{builds})=(#{skipped}) AND (#{warnings}) THEN 'success' WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index ea10d004c9c..d178ee4422b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -67,7 +67,6 @@ module Issuable scope :authored, ->(user) { where(author_id: user) } scope :recent, -> { reorder(id: :desc) } - scope :order_position_asc, -> { reorder(position: :asc) } scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } @@ -139,7 +138,6 @@ module Issuable when 'upvotes_desc' then order_upvotes_desc when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) - when 'position_asc' then order_position_asc else order_by(method) end @@ -163,9 +161,9 @@ module Issuable # milestones_due_date = 'MIN(milestones.due_date)' - order_milestone_due_asc. - order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]). - reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'), + order_milestone_due_asc + .order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]) + .reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'), Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end @@ -184,9 +182,9 @@ module Issuable "(#{highest_priority}) AS highest_priority" ] + extra_select_columns - select(select_columns.join(', ')). - group(arel_table[:id]). - reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + select(select_columns.join(', ')) + .group(arel_table[:id]) + .reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end def with_label(title, sort = nil) diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index a3472af5c55..01599ce49c6 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -40,10 +40,18 @@ module Milestoneish def issues_visible_to_user(user) memoize_per_user(user, :issues_visible_to_user) do IssuesFinder.new(user, issues_finder_params) - .execute.includes(:assignees).where(milestone_id: milestoneish_ids) + .execute.preload(:assignees).where(milestone_id: milestoneish_ids) end end + def sorted_issues(user) + issues_visible_to_user(user).preload_associations.sort('label_priority') + end + + def sorted_merge_requests + merge_requests.sort('label_priority') + end + def upcoming? start_date && start_date.future? end diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index f1d8532a6d6..7cb9a28a284 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -18,10 +18,10 @@ module RelativePositioning prev_pos = nil if self.relative_position - prev_pos = self.class. - in_projects(project.id). - where('relative_position < ?', self.relative_position). - maximum(:relative_position) + prev_pos = self.class + .in_projects(project.id) + .where('relative_position < ?', self.relative_position) + .maximum(:relative_position) end prev_pos @@ -31,10 +31,10 @@ module RelativePositioning next_pos = nil if self.relative_position - next_pos = self.class. - in_projects(project.id). - where('relative_position > ?', self.relative_position). - minimum(:relative_position) + next_pos = self.class + .in_projects(project.id) + .where('relative_position > ?', self.relative_position) + .minimum(:relative_position) end next_pos diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 63d02b76f6b..ec7796a9dbb 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -107,6 +107,14 @@ module Routable RequestStore[key] ||= uncached_full_path end + def build_full_path + if parent && path + parent.full_path + '/' + path + else + path + end + end + private def uncached_full_path @@ -135,14 +143,6 @@ module Routable end end - def build_full_path - if parent && path - parent.full_path + '/' + path - else - path - end - end - def update_route prepare_route route.save diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index b9a2d812edd..a155a064032 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -39,12 +39,12 @@ module Sortable private def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) - query = Label.select(LabelPriority.arel_table[:priority].minimum). - left_join_priorities. - joins(:label_links). - where("label_priorities.project_id = #{project_column}"). - where("label_links.target_id = #{target_column}"). - reorder(nil) + query = Label.select(LabelPriority.arel_table[:priority].minimum) + .left_join_priorities + .joins(:label_links) + .where("label_priorities.project_id = #{project_column}") + .where("label_links.target_id = #{target_column}") + .reorder(nil) query = if target_type_column diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index 83daa9b1a64..f60a0f8f438 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -27,16 +27,16 @@ module Subscribable end def subscribers(project) - subscriptions_available(project). - where(subscribed: true). - map(&:user) + subscriptions_available(project) + .where(subscribed: true) + .map(&:user) end def toggle_subscription(user, project = nil) unsubscribe_from_other_levels(user, project) - find_or_initialize_subscription(user, project). - update(subscribed: !subscribed?(user, project)) + find_or_initialize_subscription(user, project) + .update(subscribed: !subscribed?(user, project)) end def subscribe(user, project = nil) @@ -69,14 +69,14 @@ module Subscribable end def find_or_initialize_subscription(user, project) - subscriptions. - find_or_initialize_by(user_id: user.id, project_id: project.try(:id)) + subscriptions + .find_or_initialize_by(user_id: user.id, project_id: project.try(:id)) end def subscriptions_available(project) t = Subscription.arel_table - subscriptions. - where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id)))) + subscriptions + .where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id)))) end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 85e7901dfee..056c49e7162 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -58,10 +58,10 @@ class Deployment < ActiveRecord::Base def update_merge_request_metrics! return unless environment.update_merge_request_metrics? - merge_requests = project.merge_requests. - joins(:metrics). - where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }). - where("merge_request_metrics.merged_at <= ?", self.created_at) + merge_requests = project.merge_requests + .joins(:metrics) + .where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }) + .where("merge_request_metrics.merged_at <= ?", self.created_at) if previous_deployment merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at) @@ -76,17 +76,17 @@ class Deployment < ActiveRecord::Base merge_requests.map(&:id) end - MergeRequest::Metrics. - where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil). - update_all(first_deployed_to_production_at: self.created_at) + MergeRequest::Metrics + .where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil) + .update_all(first_deployed_to_production_at: self.created_at) end def previous_deployment @previous_deployment ||= - project.deployments.joins(:environment). - where(environments: { name: self.environment.name }, ref: self.ref). - where.not(id: self.id). - take + project.deployments.joins(:environment) + .where(environments: { name: self.environment.name }, ref: self.ref) + .where.not(id: self.id) + .take end def stop_action @@ -114,6 +114,17 @@ class Deployment < ActiveRecord::Base project.monitoring_service.deployment_metrics(self) end + def has_additional_metrics? + project.prometheus_service.present? + end + + def additional_metrics + return {} unless project.prometheus_service.present? + + metrics = project.prometheus_service.additional_deployment_metrics(self) + metrics&.merge(deployment_time: created_at.to_i) || {} + end + private def ref_path diff --git a/app/models/diff_viewer/added.rb b/app/models/diff_viewer/added.rb new file mode 100644 index 00000000000..1909e6ef9d8 --- /dev/null +++ b/app/models/diff_viewer/added.rb @@ -0,0 +1,8 @@ +module DiffViewer + class Added < Base + include Simple + include Static + + self.partial_name = 'added' + end +end diff --git a/app/models/diff_viewer/base.rb b/app/models/diff_viewer/base.rb new file mode 100644 index 00000000000..0cbe714288d --- /dev/null +++ b/app/models/diff_viewer/base.rb @@ -0,0 +1,87 @@ +module DiffViewer + class Base + PARTIAL_PATH_PREFIX = 'projects/diffs/viewers'.freeze + + class_attribute :partial_name, :type, :extensions, :file_types, :binary, :switcher_icon, :switcher_title + + # These limits relate to the sum of the old and new blob sizes. + # Limits related to the actual size of the diff are enforced in Gitlab::Diff::File. + class_attribute :collapse_limit, :size_limit + + delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class + + attr_reader :diff_file + + delegate :project, to: :diff_file + + def initialize(diff_file) + @diff_file = diff_file + @initially_binary = diff_file.binary? + end + + def self.partial_path + File.join(PARTIAL_PATH_PREFIX, partial_name) + end + + def self.rich? + type == :rich + end + + def self.simple? + type == :simple + end + + def self.binary? + binary + end + + def self.text? + !binary? + end + + def self.can_render?(diff_file, verify_binary: true) + can_render_blob?(diff_file.old_blob, verify_binary: verify_binary) && + can_render_blob?(diff_file.new_blob, verify_binary: verify_binary) + end + + def self.can_render_blob?(blob, verify_binary: true) + return true if blob.nil? + return false if verify_binary && binary? != blob.binary? + return true if extensions&.include?(blob.extension) + return true if file_types&.include?(blob.file_type) + + false + end + + def collapsed? + return @collapsed if defined?(@collapsed) + return @collapsed = true if diff_file.collapsed? + + @collapsed = !diff_file.expanded? && collapse_limit && diff_file.raw_size > collapse_limit + end + + def too_large? + return @too_large if defined?(@too_large) + return @too_large = true if diff_file.too_large? + + @too_large = size_limit && diff_file.raw_size > size_limit + end + + def binary_detected_after_load? + !@initially_binary && diff_file.binary? + end + + # This method is used on the server side to check whether we can attempt to + # render the diff_file at all. Human-readable error messages are found in the + # `BlobHelper#diff_render_error_reason` helper. + def render_error + if too_large? + :too_large + end + end + + def prepare! + # To be overridden by subclasses + end + end +end diff --git a/app/models/diff_viewer/client_side.rb b/app/models/diff_viewer/client_side.rb new file mode 100644 index 00000000000..cf41d07f8eb --- /dev/null +++ b/app/models/diff_viewer/client_side.rb @@ -0,0 +1,10 @@ +module DiffViewer + module ClientSide + extend ActiveSupport::Concern + + included do + self.collapse_limit = 1.megabyte + self.size_limit = 10.megabytes + end + end +end diff --git a/app/models/diff_viewer/deleted.rb b/app/models/diff_viewer/deleted.rb new file mode 100644 index 00000000000..9c129bac694 --- /dev/null +++ b/app/models/diff_viewer/deleted.rb @@ -0,0 +1,8 @@ +module DiffViewer + class Deleted < Base + include Simple + include Static + + self.partial_name = 'deleted' + end +end diff --git a/app/models/diff_viewer/image.rb b/app/models/diff_viewer/image.rb new file mode 100644 index 00000000000..759d9a36ebb --- /dev/null +++ b/app/models/diff_viewer/image.rb @@ -0,0 +1,12 @@ +module DiffViewer + class Image < Base + include Rich + include ClientSide + + self.partial_name = 'image' + self.extensions = UploaderHelper::IMAGE_EXT + self.binary = true + self.switcher_icon = 'picture-o' + self.switcher_title = 'image diff' + end +end diff --git a/app/models/diff_viewer/mode_changed.rb b/app/models/diff_viewer/mode_changed.rb new file mode 100644 index 00000000000..d487d996f8d --- /dev/null +++ b/app/models/diff_viewer/mode_changed.rb @@ -0,0 +1,8 @@ +module DiffViewer + class ModeChanged < Base + include Simple + include Static + + self.partial_name = 'mode_changed' + end +end diff --git a/app/models/diff_viewer/no_preview.rb b/app/models/diff_viewer/no_preview.rb new file mode 100644 index 00000000000..5455fee4490 --- /dev/null +++ b/app/models/diff_viewer/no_preview.rb @@ -0,0 +1,9 @@ +module DiffViewer + class NoPreview < Base + include Simple + include Static + + self.partial_name = 'no_preview' + self.binary = true + end +end diff --git a/app/models/diff_viewer/not_diffable.rb b/app/models/diff_viewer/not_diffable.rb new file mode 100644 index 00000000000..4f9638626ea --- /dev/null +++ b/app/models/diff_viewer/not_diffable.rb @@ -0,0 +1,9 @@ +module DiffViewer + class NotDiffable < Base + include Simple + include Static + + self.partial_name = 'not_diffable' + self.binary = true + end +end diff --git a/app/models/diff_viewer/renamed.rb b/app/models/diff_viewer/renamed.rb new file mode 100644 index 00000000000..f1fbfd8c6d5 --- /dev/null +++ b/app/models/diff_viewer/renamed.rb @@ -0,0 +1,8 @@ +module DiffViewer + class Renamed < Base + include Simple + include Static + + self.partial_name = 'renamed' + end +end diff --git a/app/models/diff_viewer/rich.rb b/app/models/diff_viewer/rich.rb new file mode 100644 index 00000000000..3b0ca6e4cff --- /dev/null +++ b/app/models/diff_viewer/rich.rb @@ -0,0 +1,11 @@ +module DiffViewer + module Rich + extend ActiveSupport::Concern + + included do + self.type = :rich + self.switcher_icon = 'file-text-o' + self.switcher_title = 'rendered diff' + end + end +end diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb new file mode 100644 index 00000000000..aed1a0791b1 --- /dev/null +++ b/app/models/diff_viewer/server_side.rb @@ -0,0 +1,26 @@ +module DiffViewer + module ServerSide + extend ActiveSupport::Concern + + included do + self.collapse_limit = 1.megabyte + self.size_limit = 5.megabytes + end + + def prepare! + diff_file.old_blob&.load_all_data! + diff_file.new_blob&.load_all_data! + end + + def render_error + # Files that are not stored in the repository, like LFS files and + # build artifacts, can only be rendered using a client-side viewer, + # since we do not want to read large amounts of data into memory on the + # server side. Client-side viewers use JS and can fetch the file from + # `diff_file_blob_raw_path` and `diff_file_old_blob_raw_path` using AJAX. + return :server_side_but_stored_externally if diff_file.stored_externally? + + super + end + end +end diff --git a/app/models/diff_viewer/simple.rb b/app/models/diff_viewer/simple.rb new file mode 100644 index 00000000000..65750996ee4 --- /dev/null +++ b/app/models/diff_viewer/simple.rb @@ -0,0 +1,11 @@ +module DiffViewer + module Simple + extend ActiveSupport::Concern + + included do + self.type = :simple + self.switcher_icon = 'code' + self.switcher_title = 'source diff' + end + end +end diff --git a/app/models/diff_viewer/static.rb b/app/models/diff_viewer/static.rb new file mode 100644 index 00000000000..d761328b3f6 --- /dev/null +++ b/app/models/diff_viewer/static.rb @@ -0,0 +1,10 @@ +module DiffViewer + module Static + extend ActiveSupport::Concern + + # We can always render a static viewer, even if the diff is too large. + def render_error + nil + end + end +end diff --git a/app/models/diff_viewer/text.rb b/app/models/diff_viewer/text.rb new file mode 100644 index 00000000000..98f4b2aea2a --- /dev/null +++ b/app/models/diff_viewer/text.rb @@ -0,0 +1,15 @@ +module DiffViewer + class Text < Base + include Simple + include ServerSide + + self.partial_name = 'text' + self.binary = false + + # Since the text diff viewer doesn't render the old and new blobs in full, + # we only need the limits related to the actual size of the diff which are + # already enforced in Gitlab::Diff::File. + self.collapse_limit = nil + self.size_limit = nil + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 6211a5c1e63..66c96d0f586 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -40,11 +40,12 @@ class Environment < ActiveRecord::Base scope :stopped, -> { with_state(:stopped) } scope :order_by_last_deployed_at, -> do max_deployment_id_sql = - Deployment.select(Deployment.arel_table[:id].maximum). - where(Deployment.arel_table[:environment_id].eq(arel_table[:id])). - to_sql + Deployment.select(Deployment.arel_table[:id].maximum) + .where(Deployment.arel_table[:environment_id].eq(arel_table[:id])) + .to_sql order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC')) end + scope :in_review_folder, -> { where(environment_type: "review") } state_machine :state, initial: :available do event :start do @@ -157,6 +158,16 @@ class Environment < ActiveRecord::Base project.monitoring_service.environment_metrics(self) if has_metrics? end + def has_additional_metrics? + project.prometheus_service.present? && available? && last_deployment.present? + end + + def additional_metrics + if has_additional_metrics? + project.prometheus_service.additional_environment_metrics(self) + end + end + # An environment name is not necessarily suitable for use in URLs, DNS # or other third-party contexts, so provide a slugified version. A slug has # the following properties: @@ -209,7 +220,8 @@ class Environment < ActiveRecord::Base def etag_cache_key Gitlab::Routing.url_helpers.namespace_project_environments_path( project.namespace, - project) + project, + format: :json) end private diff --git a/app/models/event.rb b/app/models/event.rb index fad6ff03927..29bc141c5cd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -376,9 +376,9 @@ class Event < ActiveRecord::Base # At this point it's possible for multiple threads/processes to try to # update the project. Only one query should actually perform the update, # hence we add the extra WHERE clause for last_activity_at. - Project.unscoped.where(id: project_id). - where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago). - update_all(last_activity_at: created_at) + Project.unscoped.where(id: project_id) + .where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago) + .update_all(last_activity_at: created_at) end def authored_by?(user) @@ -392,7 +392,7 @@ class Event < ActiveRecord::Base end def set_last_repository_updated_at - Project.unscoped.where(id: project_id). - update_all(last_repository_updated_at: created_at) + Project.unscoped.where(id: project_id) + .update_all(last_repository_updated_at: created_at) end end diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 8867ba0d2ff..532b8f4ad69 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -11,6 +11,7 @@ class GenericCommitStatus < CommitStatus def set_default_values self.context ||= 'default' self.stage ||= 'external' + self.stage_idx ||= 1000000 end def tags diff --git a/app/models/group.rb b/app/models/group.rb index 5bb2cdc5eff..0b93460d473 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -206,8 +206,8 @@ class Group < Namespace end def refresh_members_authorized_projects - UserProjectAccessChangedService.new(user_ids_for_project_authorizations). - execute + UserProjectAccessChangedService.new(user_ids_for_project_authorizations) + .execute end def user_ids_for_project_authorizations @@ -225,10 +225,10 @@ class Group < Namespace def max_member_access_for_user(user) return GroupMember::OWNER if user.admin? - members_with_parents. - where(user_id: user). - reorder(access_level: :desc). - first&. + members_with_parents + .where(user_id: user) + .reorder(access_level: :desc) + .first&. access_level || GroupMember::NO_ACCESS end diff --git a/app/models/issue.rb b/app/models/issue.rb index 693cc21bb40..3a9a6dba601 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -9,6 +9,9 @@ class Issue < ActiveRecord::Base include Spammable include FasterCacheKeys include RelativePositioning + include IgnorableColumn + + ignore_column :position DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze @@ -44,7 +47,7 @@ class Issue < ActiveRecord::Base scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } - scope :include_associations, -> { includes(:labels, project: :namespace) } + scope :preload_associations, -> { preload(:labels, project: :namespace) } after_save :expire_etag_cache @@ -121,8 +124,8 @@ class Issue < ActiveRecord::Base end def self.order_by_position_and_priority - order_labels_priority. - reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'), + order_labels_priority + .reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'), Gitlab::Database.nulls_last_order('highest_priority', 'ASC'), "id DESC") end diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb index f0b7d9914c8..49f011c113f 100644 --- a/app/models/issue_collection.rb +++ b/app/models/issue_collection.rb @@ -17,9 +17,9 @@ class IssueCollection # Given all the issue projects we get a list of projects that the current # user has at least reporter access to. - projects_with_reporter_access = user. - projects_with_reporter_access_limited_to(project_ids). - pluck(:id) + projects_with_reporter_access = user + .projects_with_reporter_access_limited_to(project_ids) + .pluck(:id) collection.select do |issue| if projects_with_reporter_access.include?(issue.project_id) diff --git a/app/models/label.rb b/app/models/label.rb index 955d6b4079b..ed6a8411da9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -46,9 +46,9 @@ class Label < ActiveRecord::Base labels = Label.arel_table priorities = LabelPriority.arel_table - label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). - on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))). - join_sources + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin) + .on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))) + .join_sources joins(label_priorities).where(priorities[:priority].eq(nil)) end @@ -57,9 +57,9 @@ class Label < ActiveRecord::Base labels = Label.arel_table priorities = LabelPriority.arel_table - label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). - on(labels[:id].eq(priorities[:label_id])). - join_sources + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin) + .on(labels[:id].eq(priorities[:label_id])) + .join_sources joins(label_priorities) end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 7126de2d488..2d5909ab25e 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -42,7 +42,7 @@ class LegacyDiffNote < Note end def for_line?(line) - !line.meta? && diff_file.line_code(line) == self.line_code + line.discussable? && diff_file.line_code(line) == self.line_code end def original_line_code diff --git a/app/models/member.rb b/app/models/member.rb index 788a32dd8e3..dc9247bc9a0 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -99,9 +99,9 @@ class Member < ActiveRecord::Base users = User.arel_table members = Member.arel_table - member_users = members.join(users, Arel::Nodes::OuterJoin). - on(members[:user_id].eq(users[:id])). - join_sources + member_users = members.join(users, Arel::Nodes::OuterJoin) + .on(members[:user_id].eq(users[:id])) + .join_sources joins(member_users) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index dd155252ad5..c099d731082 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -4,6 +4,9 @@ class MergeRequest < ActiveRecord::Base include Noteable include Referable include Sortable + include IgnorableColumn + + ignore_column :position belongs_to :target_project, class_name: "Project" belongs_to :source_project, class_name: "Project" @@ -574,8 +577,8 @@ class MergeRequest < ActiveRecord::Base messages = [title, description] messages.concat(commits.map(&:safe_message)) if merge_request_diff - Gitlab::ClosingIssueExtractor.new(project, current_user). - closed_by_message(messages.join("\n")) + Gitlab::ClosingIssueExtractor.new(project, current_user) + .closed_by_message(messages.join("\n")) else [] end @@ -768,6 +771,7 @@ class MergeRequest < ActiveRecord::Base "refs/heads/#{source_branch}", ref_path ) + update_column(:ref_fetched, true) end def ref_path @@ -775,7 +779,13 @@ class MergeRequest < ActiveRecord::Base end def ref_fetched? - project.repository.ref_exists?(ref_path) + super || + begin + computed_value = project.repository.ref_exists?(ref_path) + update_column(:ref_fetched, true) if computed_value + + computed_value + end end def ensure_ref_fetched @@ -889,7 +899,7 @@ class MergeRequest < ActiveRecord::Base !has_commits? end - def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil) + def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil) return false unless can_be_merged_by?(current_user) return true if autocomplete_precheck diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 99dd2130188..f1ee4d3f7a9 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -10,6 +10,7 @@ class MergeRequestDiff < ActiveRecord::Base VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze belongs_to :merge_request + has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) } serialize :st_commits # rubocop:disable Cop/ActiverecordSerialize serialize :st_diffs # rubocop:disable Cop/ActiverecordSerialize @@ -91,7 +92,7 @@ class MergeRequestDiff < ActiveRecord::Base head_commit_sha).diffs(options) else @raw_diffs ||= {} - @raw_diffs[options] ||= load_diffs(st_diffs, options) + @raw_diffs[options] ||= load_diffs(options) end end @@ -253,24 +254,44 @@ class MergeRequestDiff < ActiveRecord::Base update_columns_serialized(new_attributes) end - def dump_diffs(diffs) - if diffs.respond_to?(:map) - diffs.map(&:to_hash) + def create_merge_request_diff_files(diffs) + rows = diffs.map.with_index do |diff, index| + diff.to_hash.merge( + merge_request_diff_id: self.id, + relative_order: index + ) end + + Gitlab::Database.bulk_insert('merge_request_diff_files', rows) end - def load_diffs(raw, options) - if valid_raw_diff?(raw) - if paths = options[:paths] - raw = raw.select do |diff| - paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) - end - end + def load_diffs(options) + return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database - Gitlab::Git::DiffCollection.new(raw, options) - else - Gitlab::Git::DiffCollection.new([]) + raw = diffs_from_database + + if paths = options[:paths] + raw = raw.select do |diff| + paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) + end end + + Gitlab::Git::DiffCollection.new(raw, options) + end + + def diffs_from_database + return @diffs_from_database if defined?(@diffs_from_database) + + @diffs_from_database = + if st_diffs.present? + if valid_raw_diff?(st_diffs) + st_diffs + end + elsif merge_request_diff_files.present? + merge_request_diff_files + .as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS) + .map(&:with_indifferent_access) + end end # Load diffs between branches related to current merge request diff from repo @@ -285,11 +306,10 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:real_size] = diff_collection.real_size if diff_collection.any? - new_diffs = dump_diffs(diff_collection) new_attributes[:state] = :collected - end - new_attributes[:st_diffs] = new_diffs || [] + create_merge_request_diff_files(diff_collection) + end # Set our state to 'overflow' to make the #empty? and #collected? # methods (generated by StateMachine) return false. diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb new file mode 100644 index 00000000000..598ebd4d829 --- /dev/null +++ b/app/models/merge_request_diff_file.rb @@ -0,0 +1,11 @@ +class MergeRequestDiffFile < ActiveRecord::Base + include Gitlab::EncodingHelper + + belongs_to :merge_request_diff + + def utf8_diff + return '' if diff.blank? + + encode_utf8(diff) if diff.respond_to?(:encoding) + end +end diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index daafb137be4..7f7c114803d 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -7,9 +7,9 @@ class MergeRequestsClosingIssues < ActiveRecord::Base class << self def count_for_collection(ids) - group(:issue_id). - where(issue_id: ids). - pluck('issue_id', 'COUNT(*) as count') + group(:issue_id) + .where(issue_id: ids) + .pluck('issue_id', 'COUNT(*) as count') end end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index b04bed4c014..d2e2749f70d 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -98,11 +98,11 @@ class Milestone < ActiveRecord::Base if Gitlab::Database.postgresql? rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') else - rel. - group(:project_id). - having('due_date = MIN(due_date)'). - pluck(:id, :project_id, :due_date). - map(&:first) + rel + .group(:project_id) + .having('due_date = MIN(due_date)') + .pluck(:id, :project_id, :due_date) + .map(&:first) end end @@ -164,38 +164,6 @@ class Milestone < ActiveRecord::Base write_attribute(:title, sanitize_title(value)) if value.present? end - # Sorts the issues for the given IDs. - # - # This method runs a single SQL query using a CASE statement to update the - # position of all issues in the current milestone (scoped to the list of IDs). - # - # Given the ids [10, 20, 30] this method produces a SQL query something like - # the following: - # - # UPDATE issues - # SET position = CASE - # WHEN id = 10 THEN 1 - # WHEN id = 20 THEN 2 - # WHEN id = 30 THEN 3 - # ELSE position - # END - # WHERE id IN (10, 20, 30); - # - # This method expects that the IDs given in `ids` are already Fixnums. - def sort_issues(ids) - pairs = [] - - ids.each_with_index do |id, index| - pairs << id - pairs << index + 1 - end - - conditions = 'WHEN id = ? THEN ? ' * ids.length - - issues.where(id: ids). - update_all(["position = CASE #{conditions} ELSE position END", *pairs]) - end - private def milestone_format_reference(format = :iid) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b48d73dcae7..583d4fb5244 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -181,16 +181,16 @@ class Namespace < ActiveRecord::Base def ancestors return self.class.none unless parent_id - Gitlab::GroupHierarchy. - new(self.class.where(id: parent_id)). - base_and_ancestors + Gitlab::GroupHierarchy + .new(self.class.where(id: parent_id)) + .base_and_ancestors end # Returns all the descendants of the current namespace. def descendants - Gitlab::GroupHierarchy. - new(self.class.where(parent_id: id)). - base_and_descendants + Gitlab::GroupHierarchy + .new(self.class.where(parent_id: id)) + .base_and_descendants end def user_ids_for_project_authorizations @@ -253,10 +253,10 @@ class Namespace < ActiveRecord::Base end def refresh_access_of_projects_invited_groups - Group. - joins(project_group_links: :project). - where(projects: { namespace_id: id }). - find_each(&:refresh_members_authorized_projects) + Group + .joins(project_group_links: :project) + .where(projects: { namespace_id: id }) + .find_each(&:refresh_members_authorized_projects) end def remove_exports! diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 59737bb6085..2bc00a082df 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -113,7 +113,7 @@ module Network opts[:ref] = @commit.id if @filter_ref - @repo.find_commits(opts) + Gitlab::Git::Commit.find_all(@repo.raw_repository, opts) end def commits_sort_by_ref diff --git a/app/models/note.rb b/app/models/note.rb index 244bf169c29..ca6999427c0 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -32,7 +32,7 @@ class Note < ActiveRecord::Base # Banzai::ObjectRenderer attr_accessor :user_visible_reference_count - # Attribute used to store the attributes that have ben changed by slash commands. + # Attribute used to store the attributes that have ben changed by quick actions. attr_accessor :commands_changes default_value_for :system, false @@ -137,9 +137,9 @@ class Note < ActiveRecord::Base end def count_for_collection(ids, type) - user.select('noteable_id', 'COUNT(*) as count'). - group(:noteable_id). - where(noteable_type: type, noteable_id: ids) + user.select('noteable_id', 'COUNT(*) as count') + .group(:noteable_id) + .where(noteable_type: type, noteable_id: ids) end end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 42412a9a44b..b0df7aeb323 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -41,10 +41,8 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ].freeze - store :events, accessors: EMAIL_EVENTS, coder: JSON - - before_create :set_events - before_save :events_to_boolean + store :events, coder: JSON + before_save :convert_events def self.find_or_create_for(source) setting = find_or_initialize_by(source: source) @@ -56,21 +54,18 @@ class NotificationSetting < ActiveRecord::Base setting end - # Set all event attributes to false when level is not custom or being initialized for UX reasons - def set_events - return if custom? - - self.events = {} - end + # 1. Check if this event has a value stored in its database column. + # 2. If it does, return that value. + # 3. If it doesn't (the value is nil), return the value from the serialized + # JSON hash in `events`. + (EMAIL_EVENTS - [:failed_pipeline]).each do |event| + define_method(event) do + bool = super() - # Validates store accessors values as boolean - # It is a text field so it does not cast correct boolean values in JSON - def events_to_boolean - EMAIL_EVENTS.each do |event| - bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event)) - - events[event] = bool + bool.nil? ? !!events[event] : bool end + + alias_method :"#{event}?", event end # Allow people to receive failed pipeline notifications if they already have @@ -78,7 +73,23 @@ class NotificationSetting < ActiveRecord::Base # custom settings. def failed_pipeline bool = super + bool = events[:failed_pipeline] if bool.nil? bool.nil? || bool end + alias_method :failed_pipeline?, :failed_pipeline + + def event_enabled?(event) + respond_to?(event) && public_send(event) + end + + def convert_events + return if events_before_type_cast.nil? + + EMAIL_EVENTS.each do |event| + write_attribute(event, public_send(event)) + end + + write_attribute(:events, nil) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 4c394646787..1176bec8873 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -244,8 +244,8 @@ class Project < ActiveRecord::Base scope :inside_path, ->(path) do # We need routes alias rs for JOIN so it does not conflict with # includes(:route) which we use in ProjectsFinder. - joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'"). - where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%") + joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'") + .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%") end # "enabled" here means "not disabled". It includes private features! @@ -266,20 +266,49 @@ class Project < ActiveRecord::Base enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } + # Returns a collection of projects that is either public or visible to the + # logged in user. + def self.public_or_visible_to_user(user = nil) + if user + authorized = user + .project_authorizations + .select(1) + .where('project_authorizations.project_id = projects.id') + + levels = Gitlab::VisibilityLevel.levels_for_user(user) + + where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels) + else + public_to_user + end + end + # project features may be "disabled", "internal" or "enabled". If "internal", # they are only available to team members. This scope returns projects where # the feature is either enabled, or internal with permission for the user. + # + # This method uses an optimised version of `with_feature_access_level` for + # logged in users to more efficiently get private projects with the given + # feature. def self.with_feature_available_for_user(feature, user) - return with_feature_enabled(feature) if user.try(:admin?) + visible = [nil, ProjectFeature::ENABLED] - unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED]) - return unconditional if user.nil? + if user&.admin? + with_feature_enabled(feature) + elsif user + column = ProjectFeature.quoted_access_level_column(feature) - conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE) - authorized = user.authorized_projects.merge(conditional.reorder(nil)) + authorized = user.project_authorizations.select(1) + .where('project_authorizations.project_id = projects.id') - union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)]) - where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql))) + with_project_feature + .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", + visible, + ProjectFeature::PRIVATE, + authorized) + else + with_feature_access_level(feature, visible) + end end scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } @@ -321,7 +350,10 @@ class Project < ActiveRecord::Base project.run_after_commit { add_import_job } end - after_transition started: :finished, do: :reset_cache_and_import_attrs + after_transition started: :finished do |project, _| + project.reset_cache_and_import_attrs + project.perform_housekeeping + end end class << self @@ -340,14 +372,14 @@ class Project < ActiveRecord::Base # unscoping unnecessary conditions that'll be applied # when executing `where("projects.id IN (#{union.to_sql})")` projects = unscoped.select(:id).where( - ptable[:path].matches(pattern). - or(ptable[:name].matches(pattern)). - or(ptable[:description].matches(pattern)) + ptable[:path].matches(pattern) + .or(ptable[:name].matches(pattern)) + .or(ptable[:description].matches(pattern)) ) - namespaces = unscoped.select(:id). - joins(:namespace). - where(ntable[:name].matches(pattern)) + namespaces = unscoped.select(:id) + .joins(:namespace) + .where(ntable[:name].matches(pattern)) union = Gitlab::SQL::Union.new([projects, namespaces]) @@ -388,8 +420,8 @@ class Project < ActiveRecord::Base end def trending - joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id'). - reorder('trending_projects.id ASC') + joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id') + .reorder('trending_projects.id ASC') end def cached_count @@ -481,6 +513,18 @@ class Project < ActiveRecord::Base remove_import_data end + def perform_housekeeping + return unless repo_exists? + + run_after_commit do + begin + Projects::HousekeepingService.new(self).execute + rescue Projects::HousekeepingService::LeaseTaken => e + Rails.logger.info("Could not perform housekeeping for project #{self.path_with_namespace} (#{self.id}): #{e}") + end + end + end + def remove_import_data import_data&.destroy end @@ -660,7 +704,7 @@ class Project < ActiveRecord::Base end def last_activity_date - last_activity_at || updated_at + last_repository_updated_at || last_activity_at || updated_at end def project_id diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index def09675253..73302207e6b 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -7,9 +7,9 @@ class ProjectAuthorization < ActiveRecord::Base validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true def self.select_from_union(union) - select(['project_id', 'MAX(access_level) AS access_level']). - from("(#{union.to_sql}) #{ProjectAuthorization.table_name}"). - group(:project_id) + select(['project_id', 'MAX(access_level) AS access_level']) + .from("(#{union.to_sql}) #{ProjectAuthorization.table_name}") + .group(:project_id) end def self.insert_authorizations(rows, per_batch = 1000) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index e3ef4919b28..48edd0738ee 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -27,6 +27,13 @@ class ProjectFeature < ActiveRecord::Base "#{feature}_access_level".to_sym end + + def quoted_access_level_column(feature) + attribute = connection.quote_column_name(access_level_attribute(feature)) + table = connection.quote_table_name(table_name) + + "#{table}.#{attribute}" + end end # Default scopes force us to unscope here since a service may need to check @@ -83,7 +90,7 @@ class ProjectFeature < ActiveRecord::Base when DISABLED false when PRIVATE - user && (project.team.member?(user) || user.admin?) + user && (project.team.member?(user) || user.full_private_access?) when ENABLED true else diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 3edc395033c..d63d4ec2b12 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -70,7 +70,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 04a59d559ca..c52dd6ef8ef 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -61,7 +61,7 @@ module ChatMessage end def removed_branch_message - "#{user_name} removed #{ref_type} `#{ref}` from #{project_link}" + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" end def push_message @@ -102,7 +102,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 8977a7cdafe..48e7802c557 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -116,30 +116,19 @@ class KubernetesService < DeploymentService # short time later def terminals(environment) with_reactive_cache do |data| - pods = data.fetch(:pods, nil) - filter_pods(pods, app: environment.slug). - flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }. - each { |terminal| add_terminal_auth(terminal, terminal_auth) } + pods = filter_by_label(data[:pods], app: environment.slug) + terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) } + terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } end end - # Caches all pods in the namespace so other calls don't need to block on - # network access. + # Caches resources in the namespace so other calls don't need to block on + # network access def calculate_reactive_cache return unless active? && project && !project.pending_delete? - kubeclient = build_kubeclient! - - # Store as hashes, rather than as third-party types - pods = begin - kubeclient.get_pods(namespace: actual_namespace).as_json - rescue KubeException => err - raise err unless err.error_code == 404 - [] - end - # We may want to cache extra things in the future - { pods: pods } + { pods: read_pods } end TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze @@ -166,6 +155,16 @@ class KubernetesService < DeploymentService ) end + # Returns a hash of all pods in the namespace + def read_pods + kubeclient = build_kubeclient! + + kubeclient.get_pods(namespace: actual_namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + def kubeclient_ssl_options opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } @@ -181,11 +180,11 @@ class KubernetesService < DeploymentService { bearer_token: token } end - def join_api_url(*parts) + def join_api_url(api_path) url = URI.parse(api_url) prefix = url.path.sub(%r{/+\z}, '') - url.path = [prefix, *parts].join("/") + url.path = [prefix, api_path].join("/") url.to_s end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 56f42d63b2d..4d2037286a2 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,4 +1,4 @@ -class MattermostSlashCommandsService < ChatSlashCommandsService +class MattermostSlashCommandsService < SlashCommandsService include TriggersHelper prop_accessor :token @@ -20,8 +20,8 @@ class MattermostSlashCommandsService < ChatSlashCommandsService end def configure(user, params) - token = Mattermost::Command.new(user). - create(command(params)) + token = Mattermost::Command.new(user) + .create(command(params)) update(active: true, token: token) if token rescue Mattermost::Error => e diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 110b8bc209b..217f753f05f 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -28,17 +28,6 @@ class PrometheusService < MonitoringService 'Prometheus monitoring' end - def help - <<-MD.strip_heredoc - Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total` - and `container_memory_usage_bytes` from the configured Prometheus server. - - If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) - or have set up your own Prometheus server, an `environment` label is required on each metric to - [identify the Environment](https://docs.gitlab.com/ce/user/project/integrations/prometheus.html#metrics-and-labels). - MD - end - def self.to_param 'prometheus' end @@ -50,6 +39,7 @@ class PrometheusService < MonitoringService name: 'api_url', title: 'API URL', placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/', + help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.', required: true } ] @@ -65,23 +55,34 @@ class PrometheusService < MonitoringService end def environment_metrics(environment) - with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &:itself) + with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics)) end def deployment_metrics(deployment) - metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &:itself) - metrics&.merge(deployment_time: created_at.to_i) || {} + metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics)) + metrics&.merge(deployment_time: deployment.created_at.to_i) || {} + end + + def additional_environment_metrics(environment) + with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery.name, environment.id, &:itself) + end + + def additional_deployment_metrics(deployment) + with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.id, &:itself) + end + + def matched_metrics + with_reactive_cache(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, &:itself) end # Cache metrics for specific environment def calculate_reactive_cache(query_class_name, *args) return unless active? && project && !project.pending_delete? - metrics = Kernel.const_get(query_class_name).new(client).query(*args) - + data = Kernel.const_get(query_class_name).new(client).query(*args) { success: true, - metrics: metrics, + data: data, last_update: Time.now.utc } rescue Gitlab::PrometheusError => err @@ -91,4 +92,11 @@ class PrometheusService < MonitoringService def client @prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url) end + + private + + def rename_data_to_metrics(metrics) + metrics[:metrics] = metrics.delete :data + metrics + end end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 2182c1c7e4b..1c3892a3f75 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,4 +1,4 @@ -class SlackSlashCommandsService < ChatSlashCommandsService +class SlackSlashCommandsService < SlashCommandsService include TriggersHelper def title diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 8b5bc24fd3c..4592cb747a0 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -1,6 +1,6 @@ # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. -class ChatSlashCommandsService < Service +class SlashCommandsService < Service default_value_for :category, 'chat' prop_accessor :token @@ -33,10 +33,10 @@ class ChatSlashCommandsService < Service user = find_chat_user(params) if user - Gitlab::ChatCommands::Command.new(project, user, params).execute + Gitlab::SlashCommands::Command.new(project, user, params).execute else url = authorize_chat_name_url(params) - Gitlab::ChatCommands::Presenters::Access.new(url).authorize + Gitlab::SlashCommands::Presenters::Access.new(url).authorize end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index e1cc56551ba..674eacd28e8 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -172,10 +172,10 @@ class ProjectTeam return access if user_ids.empty? - users_access = project.project_authorizations. - where(user: user_ids). - group(:user_id). - maximum(:access_level) + users_access = project.project_authorizations + .where(user: user_ids) + .group(:user_id) + .maximum(:access_level) access.merge!(users_access) diff --git a/app/models/repository.rb b/app/models/repository.rb index 7460515fea8..8c24e722a8b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -241,11 +241,11 @@ class Repository cache.fetch(:"diverging_commit_counts_#{branch.name}") do # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes - number_commits_behind = raw_repository. - count_commits_between(branch.dereferenced_target.sha, root_ref_hash) + number_commits_behind = raw_repository + .count_commits_between(branch.dereferenced_target.sha, root_ref_hash) - number_commits_ahead = raw_repository. - count_commits_between(root_ref_hash, branch.dereferenced_target.sha) + number_commits_ahead = raw_repository + .count_commits_between(root_ref_hash, branch.dereferenced_target.sha) { behind: number_commits_behind, ahead: number_commits_ahead } end @@ -605,22 +605,6 @@ class Repository end end - # Returns url for submodule - # - # Ex. - # @repository.submodule_url_for('master', 'rack') - # # => git@localhost:rack.git - # - def submodule_url_for(ref, path) - if submodules(ref).any? - submodule = submodules(ref)[path] - - if submodule - submodule['url'] - end - end - end - def last_commit_for_path(sha, path) sha = last_commit_id_for_path(sha, path) commit(sha) diff --git a/app/models/todo.rb b/app/models/todo.rb index 696d139af74..7af54b2beb2 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -70,9 +70,9 @@ class Todo < ActiveRecord::Base highest_priority = highest_label_priority(params).to_sql - select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). - order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). - order('todos.created_at') + select("#{table_name}.*, (#{highest_priority}) AS highest_priority") + .order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + .order('todos.created_at') end end diff --git a/app/models/user.rb b/app/models/user.rb index 5d128e4b390..6dd1b1415d6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -53,7 +53,7 @@ class User < ActiveRecord::Base lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i) return unless lease.try_obtain - save(validate: false) + Users::UpdateService.new(self).execute(validate: false) end attr_accessor :force_random_password @@ -139,21 +139,21 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validate :namespace_uniq, if: ->(user) { user.username_changed? } + validate :namespace_uniq, if: :username_changed? validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } - validate :unique_email, if: ->(user) { user.email_changed? } - validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } - validate :owns_public_email, if: ->(user) { user.public_email_changed? } + validate :unique_email, if: :email_changed? + validate :owns_notification_email, if: :notification_email_changed? + validate :owns_public_email, if: :public_email_changed? validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :sanitize_attrs - before_validation :set_notification_email, if: ->(user) { user.email_changed? } - before_validation :set_public_email, if: ->(user) { user.public_email_changed? } + before_validation :set_notification_email, if: :email_changed? + before_validation :set_public_email, if: :public_email_changed? - after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } + after_update :update_emails_with_primary_email, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token - before_save :ensure_external_user_rights + before_save :ensure_user_rights_and_limits, if: :external_changed? after_save :ensure_namespace_correct after_initialize :set_projects_limit after_destroy :post_destroy_hook @@ -223,13 +223,13 @@ class User < ActiveRecord::Base scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) } def self.with_two_factor - joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). - where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") + .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) end def self.without_two_factor - joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). - where("u2f.id IS NULL AND otp_required_for_login = ?", false) + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") + .where("u2f.id IS NULL AND otp_required_for_login = ?", false) end # @@ -300,9 +300,9 @@ class User < ActiveRecord::Base pattern = "%#{query}%" where( - table[:name].matches(pattern). - or(table[:email].matches(pattern)). - or(table[:username].matches(pattern)) + table[:name].matches(pattern) + .or(table[:email].matches(pattern)) + .or(table[:username].matches(pattern)) ) end @@ -317,10 +317,10 @@ class User < ActiveRecord::Base matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern)) where( - table[:name].matches(pattern). - or(table[:email].matches(pattern)). - or(table[:username].matches(pattern)). - or(table[:id].in(matched_by_emails_user_ids)) + table[:name].matches(pattern) + .or(table[:email].matches(pattern)) + .or(table[:username].matches(pattern)) + .or(table[:id].in(matched_by_emails_user_ids)) ) end @@ -494,17 +494,15 @@ class User < ActiveRecord::Base def update_emails_with_primary_email primary_email_record = emails.find_by(email: email) if primary_email_record - primary_email_record.destroy - emails.create(email: email_was) - - update_secondary_emails! + Emails::DestroyService.new(self, email: email).execute + Emails::CreateService.new(self, email: email_was).execute end end # Returns the groups a user has access to def authorized_groups - union = Gitlab::SQL::Union. - new([groups.select(:id), authorized_projects.select(:namespace_id)]) + union = Gitlab::SQL::Union + .new([groups.select(:id), authorized_projects.select(:namespace_id)]) Group.where("namespaces.id IN (#{union.to_sql})") end @@ -533,8 +531,8 @@ class User < ActiveRecord::Base projects = super() if min_access_level - projects = projects. - where('project_authorizations.access_level >= ?', min_access_level) + projects = projects + .where('project_authorizations.access_level >= ?', min_access_level) end projects @@ -572,7 +570,13 @@ class User < ActiveRecord::Base end def require_password? - password_automatically_set? && !ldap_user? + password_automatically_set? && !ldap_user? && current_application_settings.signin_enabled? + end + + def require_personal_access_token? + return false if current_application_settings.signin_enabled? || ldap_user? + + PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none? end def can_change_username? @@ -619,9 +623,9 @@ class User < ActiveRecord::Base next unless project if project.repository.branch_exists?(event.branch_name) - merge_requests = MergeRequest.where("created_at >= ?", event.created_at). - where(source_project_id: project.id, - source_branch: event.branch_name) + merge_requests = MergeRequest.where("created_at >= ?", event.created_at) + .where(source_project_id: project.id, + source_branch: event.branch_name) merge_requests.empty? end end @@ -832,8 +836,8 @@ class User < ActiveRecord::Base def toggle_star(project) UsersStarProject.transaction do - user_star_project = users_star_projects. - where(project: project, user: self).lock(true).first + user_star_project = users_star_projects + .where(project: project, user: self).lock(true).first if user_star_project user_star_project.destroy @@ -869,11 +873,11 @@ class User < ActiveRecord::Base # ms on a database with a similar size to GitLab.com's database. On the other # hand, using a subquery means we can get the exact same data in about 40 ms. def contributed_projects - events = Event.select(:project_id). - contributions.where(author_id: self). - where("created_at > ?", Time.now - 1.year). - uniq. - reorder(nil) + events = Event.select(:project_id) + .contributions.where(author_id: self) + .where("created_at > ?", Time.now - 1.year) + .uniq + .reorder(nil) Project.where(id: events) end @@ -884,9 +888,9 @@ class User < ActiveRecord::Base def ci_authorized_runners @ci_authorized_runners ||= begin - runner_ids = Ci::RunnerProject. - where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})"). - select(:runner_id) + runner_ids = Ci::RunnerProject + .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") + .select(:runner_id) Ci::Runner.specific.where(id: runner_ids) end end @@ -965,7 +969,7 @@ class User < ActiveRecord::Base if attempts_exceeded? lock_access! unless access_locked? else - save(validate: false) + Users::UpdateService.new(self).execute(validate: false) end end @@ -984,6 +988,12 @@ class User < ActiveRecord::Base self.admin = (new_level == 'admin') end + # Does the user have access to all private groups & projects? + # Overridden in EE to also check auditor? + def full_private_access? + admin? + end + def update_two_factor_requirement periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period) @@ -1033,11 +1043,14 @@ class User < ActiveRecord::Base super end - def ensure_external_user_rights - return unless external? - - self.can_create_group = false - self.projects_limit = 0 + def ensure_user_rights_and_limits + if external? + self.can_create_group = false + self.projects_limit = 0 + else + self.can_create_group = gitlab_config.default_can_create_group + self.projects_limit = current_application_settings.default_projects_limit + end end def signup_domain_valid? @@ -1120,7 +1133,8 @@ class User < ActiveRecord::Base email: email, &creation_block ) - user.save(validate: false) + + Users::UpdateService.new(user).execute(validate: false) user ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index c771c22f46a..224eb3cd4d0 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -22,16 +22,16 @@ class WikiPage def self.group_by_directory(pages) return [] if pages.blank? - pages.sort_by { |page| [page.directory, page.slug] }. - group_by(&:directory). - map do |dir, pages| + pages.sort_by { |page| [page.directory, page.slug] } + .group_by(&:directory) + .map do |dir, pages| if dir.present? WikiDirectory.new(dir, pages) else pages end - end. - flatten + end + .flatten end def self.unhyphenize(name) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 4757ba71680..2683aaad981 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -10,7 +10,7 @@ class GlobalPolicy < BasePolicy can! :access_api can! :access_git can! :receive_notifications - can! :use_slash_commands + can! :use_quick_actions end end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 3959b895f44..47518dddb61 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -203,7 +203,7 @@ class ProjectPolicy < BasePolicy unless project.feature_available?(:builds, user) && repository_enabled cannot!(*named_abilities(:build)) - cannot!(*named_abilities(:pipeline)) + cannot!(*named_abilities(:pipeline) - [:read_pipeline]) cannot!(*named_abilities(:pipeline_schedule)) cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:deployment)) diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 0eddbaaaebf..eeb5399aa8b 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -1,4 +1,4 @@ -class BuildDetailsEntity < BuildEntity +class BuildDetailsEntity < JobEntity expose :coverage, :erased_at, :duration expose :tag_list, as: :tags expose :user, using: UserEntity @@ -25,7 +25,7 @@ class BuildDetailsEntity < BuildEntity end expose :raw_path do |build| - raw_namespace_project_build_path(project.namespace, project, build) + raw_namespace_project_job_path(project.namespace, project, build) end private diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb index 79b67001199..bae9932847f 100644 --- a/app/serializers/build_serializer.rb +++ b/app/serializers/build_serializer.rb @@ -1,5 +1,5 @@ class BuildSerializer < BaseSerializer - entity BuildEntity + entity JobEntity def represent_status(resource) data = represent(resource, { only: [:status] }) diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 8b3de1bed0f..e493c9162fd 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -24,6 +24,6 @@ class DeploymentEntity < Grape::Entity expose :user, using: UserEntity expose :commit, using: CommitEntity - expose :deployable, using: BuildEntity - expose :manual_actions, using: BuildEntity + expose :deployable, using: JobEntity + expose :manual_actions, using: JobEntity end diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb index 4f506f7e745..7c872a3e986 100644 --- a/app/serializers/group_entity.rb +++ b/app/serializers/group_entity.rb @@ -5,11 +5,15 @@ class GroupEntity < Grape::Entity include GroupsHelper expose :id, :name, :path, :description, :visibility - expose :web_url expose :full_name, :full_path + expose :web_url expose :parent_id expose :created_at, :updated_at + expose :group_path do |group| + group_path(group) + end + expose :permissions do expose :human_group_access do |group, options| group.group_members.find_by(user_id: request.current_user)&.human_access diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index 65b204d4dd2..bd5211b8e58 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -5,7 +5,6 @@ class IssuableEntity < Grape::Entity expose :description expose :lock_version expose :milestone_id - expose :position expose :state expose :title expose :updated_by_id diff --git a/app/serializers/build_entity.rb b/app/serializers/job_entity.rb index 67001f4547d..d6de43bcbcb 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/job_entity.rb @@ -1,11 +1,11 @@ -class BuildEntity < Grape::Entity +class JobEntity < Grape::Entity include RequestAwareEntity expose :id expose :name expose :build_path do |build| - path_to(:namespace_project_job, build) + build.target_url || path_to(:namespace_project_job, build) end expose :retry_path, if: -> (*) { retryable? } do |build| diff --git a/app/serializers/job_group_entity.rb b/app/serializers/job_group_entity.rb index 04487e59009..8554de55517 100644 --- a/app/serializers/job_group_entity.rb +++ b/app/serializers/job_group_entity.rb @@ -4,7 +4,7 @@ class JobGroupEntity < Grape::Entity expose :name expose :size expose :detailed_status, as: :status, with: StatusEntity - expose :jobs, with: BuildEntity + expose :jobs, with: JobEntity private diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 418fa9afd6e..a1d67cbc244 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,7 +3,7 @@ module Boards class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless movable_list? + issues = without_board_labels(issues) unless movable_list? || closed_list? issues = with_list_label(issues) if movable_list? issues.order_by_position_and_priority end @@ -21,7 +21,15 @@ module Boards end def movable_list? - @movable_list ||= list.present? && list.movable? + return @movable_list if defined?(@movable_list) + + @movable_list = list.present? && list.movable? + end + + def closed_list? + return @closed_list if defined?(@closed_list) + + @closed_list = list.present? && list.closed? end def filter_params diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 769749c9925..942145c4a8c 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -67,8 +67,8 @@ module Ci def update_merge_requests_head_pipeline return unless pipeline.latest? - MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref). - update_all(head_pipeline_id: @pipeline.id) + MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref) + .update_all(head_pipeline_id: @pipeline.id) end def skip_ci? diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index beb27a5a597..cf3d4aee2bc 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -3,8 +3,8 @@ module Ci def execute(project, trigger, ref, variables = nil) trigger_request = trigger.trigger_requests.create(variables: variables) - pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref). - execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) + pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref) + .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) trigger_request if pipeline.persisted? end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index d6a4280ce4c..af84d4c7427 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -54,15 +54,15 @@ module Ci def builds_for_shared_runner new_builds. # don't run projects which have not enabled shared runners and builds - joins(:project).where(projects: { shared_runners_enabled: true }). - joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id'). - where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). + joins(:project).where(projects: { shared_runners_enabled: true }) + .joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id') + .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). # Implement fair scheduling # this returns builds that are ordered by number of running builds # we prefer projects that don't use shared runners at all - joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id"). - order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') + joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id") + .order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') end def builds_for_specific_runner @@ -70,8 +70,8 @@ module Ci end def running_builds_for_shared_runners - Ci::Build.running.where(runner: Ci::Runner.shared). - group(:project_id).select(:project_id, 'count(*) AS running_builds') + Ci::Build.running.where(runner: Ci::Runner.shared) + .group(:project_id).select(:project_id, 'count(*) AS running_builds') end def new_builds diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb index 910a2a15e5d..7d45b4aa26a 100644 --- a/app/services/concerns/issues/resolve_discussions.rb +++ b/app/services/concerns/issues/resolve_discussions.rb @@ -10,9 +10,9 @@ module Issues def merge_request_to_resolve_discussions_of return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of) - @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id). - execute. - find_by(iid: merge_request_to_resolve_discussions_of_iid) + @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id) + .execute + .find_by(iid: merge_request_to_resolve_discussions_of_iid) end def discussions_to_resolve diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 46823418bb0..63b85c3de7d 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -2,7 +2,7 @@ class CreateDeploymentService attr_reader :job delegate :expanded_environment_name, - :environment_url, + :variables, :project, to: :job @@ -14,7 +14,8 @@ class CreateDeploymentService return unless executable? ActiveRecord::Base.transaction do - environment.external_url = environment_url if environment_url + environment.external_url = expanded_environment_url if + expanded_environment_url environment.fire_state_event(action) return unless environment.save @@ -49,6 +50,17 @@ class CreateDeploymentService @environment_options ||= job.options&.dig(:environment) || {} end + def expanded_environment_url + return @expanded_environment_url if defined?(@expanded_environment_url) + + @expanded_environment_url = + ExpandVariables.expand(environment_url, variables) if environment_url + end + + def environment_url + environment_options[:url] + end + def on_stop environment_options[:on_stop] end diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb new file mode 100644 index 00000000000..ace49889097 --- /dev/null +++ b/app/services/emails/base_service.rb @@ -0,0 +1,8 @@ +module Emails + class BaseService + def initialize(user, opts) + @user = user + @email = opts[:email] + end + end +end diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb new file mode 100644 index 00000000000..b6491ee9804 --- /dev/null +++ b/app/services/emails/create_service.rb @@ -0,0 +1,7 @@ +module Emails + class CreateService < ::Emails::BaseService + def execute + @user.emails.create(email: @email) + end + end +end diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb new file mode 100644 index 00000000000..d586b9dfe0c --- /dev/null +++ b/app/services/emails/destroy_service.rb @@ -0,0 +1,17 @@ +module Emails + class DestroyService < ::Emails::BaseService + def execute + Email.find_by_email!(@email).destroy && update_secondary_emails! + end + + private + + def update_secondary_emails! + result = ::Users::UpdateService.new(@user).execute do |user| + user.update_secondary_emails! + end + + result[:status] == 'success' + end + end +end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index f23a9f6d57c..bcca1386bed 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -28,8 +28,8 @@ module Files end def last_commit - @last_commit ||= Gitlab::Git::Commit. - last_for_path(@start_project.repository, @start_branch, @file_path) + @last_commit ||= Gitlab::Git::Commit + .last_for_path(@start_project.repository, @start_branch, @file_path) end def validate! diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index f080e6326a1..20d1fb29289 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -86,8 +86,8 @@ class GitPushService < BaseService push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| if commit.matches_cross_reference_regex? - ProcessCommitWorker. - perform_async(project.id, current_user.id, commit.to_hash, default) + ProcessCommitWorker + .perform_async(project.id, current_user.id, commit.to_hash, default) end end end @@ -101,12 +101,12 @@ class GitPushService < BaseService UpdateMergeRequestsWorker .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) - SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) - EventCreateService.new.push(@project, current_user, build_push_data) + Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(:push) + + SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) - Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(:push) if push_remove_branch? AfterBranchDeleteService diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 7c424fba428..9917a39b795 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -8,10 +8,12 @@ class GitTagPushService < BaseService @push_data = build_push_data EventCreateService.new.push(project, current_user, @push_data) + Ci::CreatePipelineService.new(project, current_user, @push_data).execute(:push) + SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - Ci::CreatePipelineService.new(project, current_user, @push_data).execute(:push) + ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size]) true diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index a65d6e11c47..8dd0846f3bc 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -142,10 +142,10 @@ class IssuableBaseService < BaseService LabelsFinder.new(current_user, project_id: @project.id).execute end - def merge_slash_commands_into_params!(issuable) + def merge_quick_actions_into_params!(issuable) description, command_params = - SlashCommands::InterpretService.new(project, current_user). - execute(params[:description], issuable) + QuickActions::InterpretService.new(project, current_user) + .execute(params[:description], issuable) # Avoid a description already set on an issuable to be overwritten by a nil params[:description] = description if params.key?(:description) @@ -162,7 +162,7 @@ class IssuableBaseService < BaseService end def create(issuable) - merge_slash_commands_into_params!(issuable) + merge_quick_actions_into_params!(issuable) filter_params(issuable) params.delete(:state_event) @@ -236,8 +236,9 @@ class IssuableBaseService < BaseService ) if old_assignees != issuable.assignees - assignees = old_assignees + issuable.assignees.to_a - invalidate_cache_counts(assignees.compact, issuable) + new_assignees = issuable.assignees.to_a + affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees) + invalidate_cache_counts(affected_assignees.compact, issuable) end after_update(issuable) diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 3cf4b82b9f2..718a7ac1f22 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -30,8 +30,8 @@ module Issues Discussions::ResolveService.new(project, current_user, merge_request: merge_request_to_resolve_discussions_of, - follow_up_issue: issue). - execute(discussions_to_resolve) + follow_up_issue: issue) + .execute(discussions_to_resolve) end private diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 76d0ba67b07..43b539ded53 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -26,29 +26,29 @@ module Labels 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 + 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) + 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) + 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) + LabelPriority + .where(label: label_ids) + .update_all(label_id: new_label) end def update_project_labels(label_ids) diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb index 514679ed29d..d2ece354efc 100644 --- a/app/services/labels/transfer_service.rb +++ b/app/services/labels/transfer_service.rb @@ -41,16 +41,16 @@ module Labels end def group_labels_applied_to_issues - Label.joins(:issues). - where( + Label.joins(:issues) + .where( issues: { project_id: project.id }, labels: { type: 'GroupLabel', group_id: old_group.id } ) end def group_labels_applied_to_merge_requests - Label.joins(:merge_requests). - where( + Label.joins(:merge_requests) + .where( merge_requests: { target_project_id: project.id }, labels: { type: 'GroupLabel', group_id: old_group.id } ) @@ -64,15 +64,15 @@ module Labels end def update_label_links(labels, old_label_id:, new_label_id:) - LabelLink.joins(:label). - merge(labels). - where(label_id: old_label_id). - update_all(label_id: new_label_id) + LabelLink.joins(:label) + .merge(labels) + .where(label_id: old_label_id) + .update_all(label_id: new_label_id) end def update_label_priorities(old_label_id:, new_label_id:) - LabelPriority.where(project_id: project.id, label_id: old_label_id). - update_all(label_id: new_label_id) + LabelPriority.where(project_id: project.id, label_id: old_label_id) + .update_all(label_id: new_label_id) end end end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index f846d72498f..de3a252d6c6 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -26,30 +26,30 @@ module Members def unassign_issues_and_merge_requests(member) if member.is_a?(GroupMember) - issues = Issue.unscoped.select(1). - joins(:project). - where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) + issues = Issue.unscoped.select(1) + .joins(:project) + .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped. - where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues). - delete_all + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all - MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). - execute. - update_all(assignee_id: nil) + MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id) + .execute + .update_all(assignee_id: nil) else project = member.source # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X - issues = Issue.unscoped.select(1). - where('issues.id = issue_assignees.issue_id'). - where(project_id: project.id) + issues = Issue.unscoped.select(1) + .where('issues.id = issue_assignees.issue_id') + .where(project_id: project.id) # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped. - where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues). - delete_all + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) end diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb index c2c335b8461..6b6e231f4f9 100644 --- a/app/services/merge_requests/conflicts/resolve_service.rb +++ b/app/services/merge_requests/conflicts/resolve_service.rb @@ -27,10 +27,10 @@ module MergeRequests tree: merge_index.write_tree(rugged) } - conflicts_for_resolution. - project. - repository. - resolve_conflicts(current_user, merge_request.source_branch, commit_params) + conflicts_for_resolution + .project + .repository + .resolve_conflicts(current_user, merge_request.source_branch, commit_params) end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index fac3ac7a4c7..b247cb89e5e 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -61,8 +61,8 @@ module MergeRequests MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? - DeleteBranchService.new(@merge_request.source_project, branch_deletion_user). - execute(merge_request.source_branch) + DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) + .execute(merge_request.source_branch) end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 81d217929d5..e0e7c43f802 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -43,9 +43,9 @@ module MergeRequests end filter_merge_requests(merge_requests).each do |merge_request| - MergeRequests::PostMergeService. - new(merge_request.target_project, @current_user). - execute(merge_request) + MergeRequests::PostMergeService + .new(merge_request.target_project, @current_user) + .execute(merge_request) end end @@ -56,8 +56,8 @@ module MergeRequests # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too def reload_merge_requests - merge_requests = @project.merge_requests.opened. - by_source_or_target_branch(@branch_name).to_a + merge_requests = @project.merge_requests.opened + .by_source_or_target_branch(@branch_name).to_a # Fork merge requests merge_requests += MergeRequest.opened diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 5c843a258fb..75a65aecd1a 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -7,7 +7,7 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) - merge_from_slash_command(merge_request) if params[:merge] + merge_from_quick_action(merge_request) if params[:merge] if merge_request.closed_without_fork? params.except!(:target_branch, :force_remove_source_branch) @@ -74,9 +74,9 @@ module MergeRequests end end - def merge_from_slash_command(merge_request) + def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) - return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha) + return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) merge_request.update(merge_error: nil) diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index f3954f6f8c4..06971483992 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -9,11 +9,11 @@ module Notes # We execute commands (extracted from `params[:note]`) on the noteable # **before** we save the note because if the note consists of commands # only, there is no need be create a note! - slash_commands_service = SlashCommandsService.new(project, current_user) + quick_actions_service = QuickActionsService.new(project, current_user) - if slash_commands_service.supported?(note) + if quick_actions_service.supported?(note) options = { merge_request_diff_head_sha: merge_request_diff_head_sha } - content, command_params = slash_commands_service.extract_commands(note, options) + content, command_params = quick_actions_service.extract_commands(note, options) only_commands = content.empty? @@ -30,7 +30,7 @@ module Notes end if command_params.present? - slash_commands_service.execute(command_params, note) + quick_actions_service.execute(command_params, note) # We must add the error after we call #save because errors are reset # when #save is called diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/quick_actions_service.rb index ad1e6f6774a..a8d0cc15527 100644 --- a/app/services/notes/slash_commands_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -1,5 +1,5 @@ module Notes - class SlashCommandsService < BaseService + class QuickActionsService < BaseService UPDATE_SERVICES = { 'Issue' => Issues::UpdateService, 'MergeRequest' => MergeRequests::UpdateService @@ -22,8 +22,8 @@ module Notes def extract_commands(note, options = {}) return [note.note, {}] unless supported?(note) - SlashCommands::InterpretService.new(project, current_user, options). - execute(note.note, note.noteable) + QuickActions::InterpretService.new(project, current_user, options) + .execute(note.note, note.noteable) end def execute(command_params, note) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 988bd0a7cdb..8d1820bc504 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -8,7 +8,7 @@ class NotificationRecipientService @project = project end - def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true) + def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true) custom_action = build_custom_key(action, target) recipients = target.participants(current_user) @@ -59,7 +59,7 @@ class NotificationRecipientService return [] if notification_setting.mention? || notification_setting.disabled? - return [] if notification_setting.custom? && !notification_setting.public_send(custom_action) + return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) @@ -176,7 +176,7 @@ class NotificationRecipientService if notification_level settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) - settings = settings.select { |setting| setting.events[action] } if action.present? + settings = settings.select { |setting| setting.event_enabled?(action) } if action.present? settings.map(&:user_id) else resource.notification_settings.pluck(:user_id) @@ -225,7 +225,7 @@ class NotificationRecipientService def user_ids_with_global_level_custom(ids, action) settings = settings_with_global_level_of(:custom, ids) - settings = settings.select { |setting| setting.events[action] } + settings = settings.select { |setting| setting.event_enabled?(action) } settings.map(&:user_id) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 646ccbdb2bf..3a98a5f6b64 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -273,7 +273,7 @@ class NotificationService end def issue_moved(issue, new_issue, current_user) - recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user) + recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user, action: 'moved') recipients.map do |recipient| email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index 10d45bbf73c..4ee2c1796bd 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -1,6 +1,6 @@ class PreviewMarkdownService < BaseService def execute - text, commands = explain_slash_commands(params[:text]) + text, commands = explain_quick_actions(params[:text]) users = find_user_references(text) success( @@ -12,11 +12,11 @@ class PreviewMarkdownService < BaseService private - def explain_slash_commands(text) + def explain_quick_actions(text) return text, [] unless %w(Issue MergeRequest).include?(commands_target_type) - slash_commands_service = SlashCommands::InterpretService.new(project, current_user) - slash_commands_service.explain(text, find_commands_target) + quick_actions_service = QuickActions::InterpretService.new(project, current_user) + quick_actions_service.explain(text, find_commands_target) end def find_user_references(text) @@ -36,10 +36,10 @@ class PreviewMarkdownService < BaseService end def commands_target_type - params[:slash_commands_target_type] + params[:quick_actions_target_type] end def commands_target_id - params[:slash_commands_target_id] + params[:quick_actions_target_id] end end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 015f2828921..fc85f398935 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -32,7 +32,7 @@ module Projects issuable: noteable, current_user: current_user } - SlashCommands::InterpretService.command_definitions.map do |definition| + QuickActions::InterpretService.command_definitions.map do |definition| next unless definition.available?(opts) definition.to_h(opts) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 1c24b27a870..fd701e33524 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -12,87 +12,121 @@ module Projects TransferError = Class.new(StandardError) def execute(new_namespace) - if new_namespace.blank? + @new_namespace = new_namespace + + if @new_namespace.blank? raise TransferError, 'Please select a new namespace for your project.' end - unless allowed_transfer?(current_user, project, new_namespace) + + unless allowed_transfer?(current_user, project) raise TransferError, 'Transfer failed, please contact an admin.' end - transfer(project, new_namespace) + + transfer(project) + + true rescue Projects::TransferService::TransferError => ex project.reload project.errors.add(:new_namespace, ex.message) false end - def transfer(project, new_namespace) - old_namespace = project.namespace + private - Project.transaction do - old_path = project.path_with_namespace - old_group = project.group - new_path = File.join(new_namespace.try(:full_path) || '', project.path) + def transfer(project) + @old_path = project.path_with_namespace + @old_group = project.group + @new_path = File.join(@new_namespace.try(:full_path) || '', project.path) + @old_namespace = project.namespace - if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? - raise TransferError.new("Project with same path in target namespace already exists") - end + if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists? + raise TransferError.new("Project with same path in target namespace already exists") + end - if project.has_container_registry_tags? - # we currently doesn't support renaming repository if it contains tags in container registry - raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') - end + if project.has_container_registry_tags? + # We currently don't support renaming repository if it contains tags in container registry + raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') + end - project.expire_caches_before_rename(old_path) + attempt_transfer_transaction + end + + def attempt_transfer_transaction + Project.transaction do + project.expire_caches_before_rename(@old_path) - # Apply new namespace id and visibility level - project.namespace = new_namespace - project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? - project.save! + update_namespace_and_visibility(@new_namespace) # Notifications - project.send_move_instructions(old_path) + project.send_move_instructions(@old_path) # Move main repository - unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path) + unless move_repo_folder(@old_path, @new_path) raise TransferError.new('Cannot move project') end # Move wiki repo also if present - gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") + move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") # Move missing group labels to project - Labels::TransferService.new(current_user, old_group, project).execute + Labels::TransferService.new(current_user, @old_group, project).execute # Move uploads - Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) + Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) # Move pages - Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) + Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) - project.old_path_with_namespace = old_path + project.old_path_with_namespace = @old_path - SystemHooksService.new.execute_hooks_for(project, :transfer) + execute_system_hooks end - - refresh_permissions(old_namespace, new_namespace) - - true + rescue Exception # rubocop:disable Lint/RescueException + rollback_side_effects + raise + ensure + refresh_permissions end - def allowed_transfer?(current_user, project, namespace) - namespace && + def allowed_transfer?(current_user, project) + @new_namespace && can?(current_user, :change_namespace, project) && - namespace.id != project.namespace_id && - current_user.can?(:create_projects, namespace) + @new_namespace.id != project.namespace_id && + current_user.can?(:create_projects, @new_namespace) end - def refresh_permissions(old_namespace, new_namespace) + def update_namespace_and_visibility(to_namespace) + # Apply new namespace id and visibility level + project.namespace = to_namespace + project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group? + project.save! + end + + def refresh_permissions # This ensures we only schedule 1 job for every user that has access to # the namespaces. - user_ids = old_namespace.user_ids_for_project_authorizations | - new_namespace.user_ids_for_project_authorizations + user_ids = @old_namespace.user_ids_for_project_authorizations | + @new_namespace.user_ids_for_project_authorizations UserProjectAccessChangedService.new(user_ids).execute end + + def rollback_side_effects + rollback_folder_move + update_namespace_and_visibility(@old_namespace) + end + + def rollback_folder_move + move_repo_folder(@new_path, @old_path) + move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki") + end + + def move_repo_folder(from_name, to_name) + gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) + end + + def execute_system_hooks + SystemHooksService.new.execute_hooks_for(project, :transfer) + end end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index b6b411d2185..6816b137361 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -1,13 +1,13 @@ -module SlashCommands +module QuickActions class InterpretService < BaseService - include Gitlab::SlashCommands::Dsl + include Gitlab::QuickActions::Dsl attr_reader :issuable # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. def execute(content, issuable) - return [content, {}] unless current_user.can?(:use_slash_commands) + return [content, {}] unless current_user.can?(:use_quick_actions) @issuable = issuable @updates = {} @@ -20,7 +20,7 @@ module SlashCommands # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and array of changes explained. def explain(content, issuable) - return [content, []] unless current_user.can?(:use_slash_commands) + return [content, []] unless current_user.can?(:use_quick_actions) @issuable = issuable @@ -32,7 +32,7 @@ module SlashCommands private def extractor - Gitlab::SlashCommands::Extractor.new(self.class.command_definitions) + Gitlab::QuickActions::Extractor.new(self.class.command_definitions) end desc do @@ -71,7 +71,7 @@ module SlashCommands last_diff_sha = params && params[:merge_request_diff_head_sha] issuable.is_a?(MergeRequest) && issuable.persisted? && - issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) + issuable.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) end command :merge do @updates[:merge] = params[:merge_request_diff_head_sha] @@ -410,7 +410,7 @@ module SlashCommands params '@user' command :cc - desc 'Define target branch for MR' + desc 'Set target branch' explanation do |branch_name| "Sets target branch to #{branch_name}." end diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index 1756da9e519..674792f6138 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -19,8 +19,8 @@ module Tags if new_tag if release_description - CreateReleaseService.new(@project, @current_user). - execute(tag_name, release_description) + CreateReleaseService.new(@project, @current_user) + .execute(tag_name, release_description) end success.merge(tag: new_tag) diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 363135ef09b..ff234a3440f 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -1,5 +1,4 @@ module Users - # Service for building a new user. class BuildService < BaseService def initialize(current_user, params = {}) @current_user = current_user diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb index e22f7225ae2..74abc017cea 100644 --- a/app/services/users/create_service.rb +++ b/app/services/users/create_service.rb @@ -1,5 +1,4 @@ module Users - # Service for creating a new user. class CreateService < BaseService def initialize(current_user, params = {}) @current_user = current_user diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 3e07b811027..f028f5eb0d4 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -34,7 +34,7 @@ module Users # Keep trying until we obtain the lease. If we don't do so we may end up # not updating the list of authorized projects properly. To prevent # hammering Redis too much we'll wait for a bit between retries. - sleep(1) + sleep(0.1) end begin diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb new file mode 100644 index 00000000000..dfbd6016c3f --- /dev/null +++ b/app/services/users/update_service.rb @@ -0,0 +1,34 @@ +module Users + class UpdateService < BaseService + def initialize(user, params = {}) + @user = user + @params = params.dup + end + + def execute(validate: true, &block) + yield(@user) if block_given? + + assign_attributes(&block) + + if @user.save(validate: validate) + success + else + error(@user.errors.full_messages.uniq.join('. ')) + end + end + + def execute!(*args, &block) + result = execute(*args, &block) + + raise ActiveRecord::RecordInvalid.new(@user) unless result[:status] == :success + + true + end + + private + + def assign_attributes(&block) + @user.assign_attributes(params) if params.any? + end + end +end diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb index 27ac60637fd..4688aabc2a8 100644 --- a/app/validators/dynamic_path_validator.rb +++ b/app/validators/dynamic_path_validator.rb @@ -26,7 +26,7 @@ class DynamicPathValidator < ActiveModel::EachValidator end def path_valid_for_record?(record, value) - full_path = record.respond_to?(:full_path) ? record.full_path : value + full_path = record.respond_to?(:build_full_path) ? record.build_full_path : value return true unless full_path diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index d552704df88..b21d5665970 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -180,11 +180,25 @@ .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled + + %fieldset + %legend Help Page .form-group = f.label :help_page_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :help_page_text, class: 'form-control', rows: 4 .help-block Markdown enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :help_page_hide_commercial_content do + = f.check_box :help_page_hide_commercial_content + Hide marketing-related entries from help + .form-group + = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block' + %span.help-block#support_help_block Alternate support URL for help page %fieldset %legend Pages @@ -299,8 +313,9 @@ %fieldset %legend Metrics - Prometheus %p - Setup Prometheus to measure a variety of statistics that partially overlap and complement Influx based metrics. - This setting requires a + Enable a Prometheus metrics endpoint at `#{metrics_path}` to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available + = link_to 'here', admin_health_check_path + \. This setting requires a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') @@ -310,6 +325,10 @@ = f.label :prometheus_metrics_enabled do = f.check_box :prometheus_metrics_enabled Enable Prometheus Metrics + - unless Gitlab::Metrics.metrics_folder_present? + .help-block + %strong.cred WARNING: + Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory. %fieldset %legend Background Jobs diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 2269fb1fd8c..5a4ed1c3a2a 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -21,11 +21,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.text_field :color, class: "form-control" + = f.color_field :color, class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.text_field :font, class: "form-control" + = f.color_field :font, class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index e242e851b4d..2da8f615470 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -58,20 +58,23 @@ %br - .table-holder - %table.table - %thead - %tr - %th Type - %th Runner token - %th Description - %th Version - %th Projects - %th Jobs - %th Tags - %th Last contact - %th + - if @runners.any? + .table-holder + %table.table + %thead + %tr + %th Type + %th Runner token + %th Description + %th Version + %th Projects + %th Jobs + %th Tags + %th Last contact + %th - - @runners.each do |runner| - = render "admin/runners/runner", runner: runner - = paginate @runners, theme: "gitlab" + - @runners.each do |runner| + = render "admin/runners/runner", runner: runner + = paginate @runners, theme: "gitlab" + - else + .nothing-here-block No runners found diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml index 086bb8e083d..a508b7537a2 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.haml +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -1,16 +1,15 @@ -.center - - if @resource.unconfirmed_email.present? - #content - %h2= @resource.unconfirmed_email - %p Click the link below to confirm your email address. - #cta - = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) - - else - #content - - if Gitlab.com? - %h2 Thanks for signing up to GitLab! - - else - %h2 Welcome, #{@resource.name}! - %p To get started, click the link below to confirm your account. - #cta - = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) +- if @resource.unconfirmed_email.present? + #content + = email_default_heading(@resource.unconfirmed_email) + %p Click the link below to confirm your email address. + #cta + = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) +- else + #content + - if Gitlab.com? + = email_default_heading('Thanks for signing up to GitLab!') + - else + = email_default_heading("Welcome, #{@resource.name}!") + %p To get started, click the link below to confirm your account. + #cta + = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) diff --git a/app/views/devise/mailer/password_change.html.haml b/app/views/devise/mailer/password_change.html.haml index 3349ee84807..5ec515285f2 100644 --- a/app/views/devise/mailer/password_change.html.haml +++ b/app/views/devise/mailer/password_change.html.haml @@ -1,10 +1,8 @@ -.center - #content - %h2 Hello, #{@resource.name}! - %p - The password for your GitLab account on - #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)} - has successfully been changed. - %p - If you did not initiate this change, please contact your administrator - immediately. += email_default_heading("Hello, #{@resource.name}!") +%p + The password for your GitLab account on + #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)} + has successfully been changed. +%p + If you did not initiate this change, please contact your administrator + immediately. diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml index e91c9522520..47e192afa52 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.haml +++ b/app/views/devise/mailer/reset_password_instructions.html.haml @@ -1,12 +1,10 @@ -.center - #content - %h2 Hello, #{@resource.name}! - %p - Someone, hopefully you, has requested to reset the password for your - GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}. - %p - If you did not perform this request, you can safely ignore this email. - %p - Otherwise, click the link below to complete the process. - #cta - = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token)) += email_default_heading("Hello, #{@resource.name}!") +%p + Someone, hopefully you, has requested to reset the password for your + GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}. +%p + If you did not perform this request, you can safely ignore this email. +%p + Otherwise, click the link below to complete the process. +#cta + = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token)) diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml index 9990d1ccac6..79e3a35cc9a 100644 --- a/app/views/devise/mailer/unlock_instructions.html.haml +++ b/app/views/devise/mailer/unlock_instructions.html.haml @@ -1,9 +1,8 @@ -.center - #content - %h2 Hello, #{@resource.name}! - %p - Your GitLab account has been locked due to an excessive amount of unsuccessful - sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)} - or you may click the link below to unlock now. - #cta - = link_to('Unlock account', unlock_url(@resource, unlock_token: @token)) +#content + = email_default_heading("Hello, #{@resource.name}!") + %p + Your GitLab account has been locked due to an excessive amount of unsuccessful + sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)} + or you may click the link below to unlock now. + #cta + = link_to('Unlock account', unlock_url(@resource, unlock_token: @token)) diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 70042dee20f..4a41be972da 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -2,8 +2,9 @@ - blob = discussion.blob .diff-file.file-holder - .js-file-title.file-title - = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false + .js-file-title.file-title.file-title-flex-parent + .file-header-content + = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false .diff-content.code.js-syntax-highlight %table diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 41f54f6bf42..181c7bee702 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -3,7 +3,7 @@ .avatar-container.s70.group-avatar = image_tag group_icon(@group), class: "avatar s70 avatar-tile" %h1.group-title - @#{@group.path} + = @group.name %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } = visibility_level_icon(@group.visibility_level, fw: false) diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 8fe0bd149f3..45e39252e16 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -18,5 +18,4 @@ - if current_user To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. - .prepend-top-default - = render 'shared/merge_requests' + = render 'shared/merge_requests' diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 31d0e589c26..c25eae63eec 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -1,4 +1,9 @@ %div +- if current_application_settings.help_page_text.present? + = markdown_field(current_application_settings, :help_page_text) + %hr + +- unless current_application_settings.help_page_hide_commercial_content? %h1 GitLab Community Edition @@ -18,13 +23,9 @@ Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. - - if current_application_settings.help_page_text.present? - %hr - = markdown_field(current_application_settings, :help_page_text) - -%hr + %hr -.row +.row.prepend-top-default .col-md-8 .documentation-index = markdown(@help_index) @@ -33,8 +34,9 @@ .panel-heading Quick help %ul.well-list - %li= link_to 'See our website for getting help', promo_url + '/getting-help/' + %li= link_to 'See our website for getting help', support_url %li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)' %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.toggleHelp()' - %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/' - %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare' + - unless current_application_settings.help_page_hide_commercial_content? + %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/' + %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare' diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index f6ebd76af9d..c07c148a12a 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,3 @@ - page_title @path.split("/").reverse.map(&:humanize) -.documentation.wiki +.documentation.wiki.prepend-top-default = markdown @markdown diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml index 3a7e0929c16..bcd2f03e83c 100644 --- a/app/views/layouts/_broadcast.html.haml +++ b/app/views/layouts/_broadcast.html.haml @@ -1 +1,2 @@ -= broadcast_message +- BroadcastMessage.current.each do |message| + = broadcast_message(message) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index eea33b5966f..eabc9a3b01c 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -30,6 +30,10 @@ = stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag 'peek' if peek_enabled? + - if show_new_nav? + = stylesheet_link_tag "new_nav", media: "all" + = stylesheet_link_tag "new_sidebar", media: "all" + = Gon::Base.render_data = webpack_bundle_tag "runtime" diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml new file mode 100644 index 00000000000..983ed22a506 --- /dev/null +++ b/app/views/layouts/_mailer.html.haml @@ -0,0 +1,74 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } + %tbody + %tr.line + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } + %tr.header + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + = header_logo + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } + %tbody + = yield + + %tr.footer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ + %div + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications + · + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host + + = yield :additional_footer diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index b7df11681d3..62a76a1b00e 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,11 +1,15 @@ -.page-with-sidebar{ class: page_gutter_class } - - if defined?(nav) && nav - .layout-nav - .container-fluid - = render "layouts/nav/#{nav}" - - if content_for?(:sub_nav) - = yield :sub_nav - .content-wrapper{ class: layout_nav_class } +.page-with-sidebar{ class: "#{('page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar)} #{page_gutter_class}" } + - if show_new_nav? + - if defined?(nav) && nav + = render "layouts/nav/#{nav}" + - else + - if defined?(nav) && nav + .layout-nav + .container-fluid + = render "layouts/nav/#{nav}" + - if content_for?(:sub_nav) + = yield :sub_nav + .content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" } .alert-wrapper = render "layouts/broadcast" = render "layouts/flash" diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 87064cc9b3f..ae9eee215e0 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,5 +1,9 @@ - page_title "Admin Area" - header_title "Admin Area", admin_root_path -- nav "admin" +- if show_new_nav? + - nav "new_admin_sidebar" + - @new_sidebar = true +- else + - nav "admin" = render template: "layouts/application" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 2b07273a0a8..d879df8fc82 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,7 +4,10 @@ %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' - = render "layouts/header/default", title: header_title + - if show_new_nav? + = render "layouts/header/new" + - else + = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml deleted file mode 100644 index e1e1f9ae516..00000000000 --- a/app/views/layouts/devise_mailer.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -!!! 5 -%html - %head - %meta{ content: 'text/html; charset=UTF-8', 'http-equiv'=> 'Content-Type' } - = stylesheet_link_tag 'mailers/devise' - - %body - %table#wrapper - %tr - %td - %table#header - %td{ valign: "top" } - = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark') - - %table#body - %tr - %td#body-container - = yield - - - if Gitlab.com? - %table#footer - %tr - %td#tanuki - = image_tag('mailers/gitlab_tanuki_2x.png', alt: 'GitLab Logo') - %tr - %td#tagline - Everyone can contribute - %tr - %td#social - = link_to 'Blog', 'https://about.gitlab.com/blog/' - = link_to 'Twitter', 'https://twitter.com/gitlab' - = link_to 'Facebook', 'https://www.facebook.com/gitlab/' - = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg' - = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com' diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index f06acc98ca1..35abfa0e80c 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,6 +1,10 @@ - page_title @group.name - page_description @group.description unless page_description - header_title group_title(@group) unless header_title -- nav "group" +- if show_new_nav? + - nav "new_group_sidebar" + - @new_sidebar = true +- else + - nav "group" = render template: "layouts/application" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 249253f4906..f056c0af968 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -74,6 +74,9 @@ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li = link_to "Settings", profile_path + - if can_toggle_new_nav? + %li + = link_to "Turn on new nav", profile_preferences_path(anchor: "new-navigation") %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml new file mode 100644 index 00000000000..c0833c64911 --- /dev/null +++ b/app/views/layouts/header/_new.html.haml @@ -0,0 +1,93 @@ +%header.navbar.navbar-gitlab.navbar-gitlab-new{ class: nav_header_class } + %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content + .container-fluid + .header-content + .title-container + %h1.title + = link_to root_path, title: 'Dashboard' do + = brand_header_logo + %span.hidden-xs + GitLab + + - if current_user + = render "layouts/nav/new_dashboard" + - else + = render "layouts/nav/new_explore" + + .navbar-collapse.collapse + %ul.nav.navbar-nav + %li.hidden-sm.hidden-xs + = render 'layouts/search' unless current_controller?(:search) + %li.visible-sm-inline-block.visible-xs-inline-block + = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('search') + - if current_user + - if session[:impersonator_id] + %li.impersonation + = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = icon('user-secret fw') + - if current_user.admin? + %li + = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('wrench fw') + = render 'layouts/header/new_dropdown' + - if Gitlab::Sherlock.enabled? + %li + = link_to sherlock_transactions_path, title: 'Sherlock Transactions', + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('tachometer fw') + %li + = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('hashtag fw') + - issues_count = assigned_issuables_count(:issues) + %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } + = number_with_delimiter(issues_count) + %li + = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = custom_icon('mr_bold') + - merge_requests_count = assigned_issuables_count(:merge_requests) + %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } + = number_with_delimiter(merge_requests_count) + %li + = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('check-circle fw') + %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } + = todos_count_format(todos_pending_count) + %li.header-user.dropdown + = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do + = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" + = icon('chevron-down') + .dropdown-menu-nav.dropdown-menu-align-right + %ul + %li.current-user + .user-name.bold + = current_user.name + @#{current_user.username} + %li.divider + %li + = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } + %li + = link_to "Settings", profile_path + %li + = link_to "Turn off new nav", profile_preferences_path(anchor: "new-navigation") + %li.divider + %li + = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" + - else + %li + %div + = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' + + %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } + %span.sr-only Toggle navigation + = icon('ellipsis-v', class: 'js-navbar-toggle-right') + = icon('times', class: 'js-navbar-toggle-left', style: 'display: none;') + + = yield :header_content + += render 'shared/outdated_browser' + +- if @project && !@project.empty_repo? + - if ref = @ref || @project.repository.root_ref + :javascript + var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, ref)}"; diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index c7302414386..9ff1164f2ee 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,10 +1,14 @@ %li.header-new.dropdown = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do - = icon('plus fw') - = icon('caret-down') + - if show_new_nav? + = icon('plus') + = icon('chevron-down') + - else + = icon('plus fw') + = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul - - if @group + - if @group&.persisted? - create_group_project = can?(current_user, :create_projects, @group) - create_group_subgroup = can?(current_user, :create_subgroup, @group) - if create_group_project || create_group_subgroup @@ -18,7 +22,7 @@ %li.divider %li.dropdown-bold-header GitLab - - if @project && @project.persisted? + - if @project&.persisted? - create_project_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - create_project_snippet = can?(current_user, :create_project_snippet, @project) diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 53268cc22f8..28dcbce7183 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -1,72 +1 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{ lang: "en" } - %head - %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ - %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ - %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ - %title= message.subject - :css - /* CLIENT-SPECIFIC STYLES */ - body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } - img { -ms-interpolation-mode: bicubic; } - - /* iOS BLUE LINKS */ - a[x-apple-data-detectors] { - color: inherit !important; - text-decoration: none !important; - font-size: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; - } - - /* ANDROID MARGIN HACK */ - body { margin:0 !important; } - div[style*="margin: 16px 0"] { margin:0 !important; } - - @media only screen and (max-width: 639px) { - body, #body { - min-width: 320px !important; - } - table.wrapper { - width: 100% !important; - min-width: 320px !important; - } - table.wrapper > tbody > tr > td { - border-left: 0 !important; - border-right: 0 !important; - border-radius: 0 !important; - padding-left: 10px !important; - padding-right: 10px !important; - } - } - %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } - %tbody - %tr.line - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } - %tr.header - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - = header_logo - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } - %tbody - = yield - - %tr.footer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ - %div - %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications - · - %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help - %div - You're receiving this email because of your account on - = succeed "." do - %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host += render 'layouts/mailer' diff --git a/app/views/layouts/mailer/devise.html.haml b/app/views/layouts/mailer/devise.html.haml new file mode 100644 index 00000000000..054b2c2fa26 --- /dev/null +++ b/app/views/layouts/mailer/devise.html.haml @@ -0,0 +1,21 @@ +- if Gitlab.com? + - content_for :additional_footer do + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %div + Everyone can contribute + %div + = link_to 'Blog', 'https://about.gitlab.com/blog/', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'Twitter', 'https://twitter.com/gitlab', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'Facebook', 'https://www.facebook.com/gitlab/', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com', style: "color:#3777b0;text-decoration:none;" + += render layout: 'layouts/mailer' do + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } + = yield diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml new file mode 100644 index 00000000000..f995145917c --- /dev/null +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -0,0 +1,119 @@ +.nav-sidebar + %ul.sidebar-top-level-items + = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do + %span + Overview + + %ul.sidebar-sub-level-items + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Overview + = nav_link(controller: [:admin, :projects]) do + = link_to admin_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'builds#index' do + = link_to admin_jobs_path, title: 'Jobs' do + %span + Jobs + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners + = nav_link path: 'cohorts#index' do + = link_to admin_cohorts_path, title: 'Cohorts' do + %span + Cohorts + + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_conversational_development_index_path, title: 'Monitoring' do + %span + Monitoring + + %ul.sidebar-sub-level-items + = nav_link(controller: :conversational_development_index) do + = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do + %span + ConvDev Index + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles + + = nav_link(controller: :broadcast_messages) do + = link_to admin_broadcast_messages_path, title: 'Messages' do + %span + Messages + = nav_link(controller: [:hooks, :hook_logs]) do + = link_to admin_hooks_path, title: 'Hooks' do + %span + System Hooks + + = nav_link(controller: :applications) do + = link_to admin_applications_path, title: 'Applications' do + %span + Applications + + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do + %span + Abuse Reports + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + + - if akismet_enabled? + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + %span + Spam Logs + + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + %span + Deploy Keys + + = nav_link(controller: :services) do + = link_to admin_application_settings_services_path, title: 'Service Templates' do + %span + Service Templates + + = nav_link(controller: :labels) do + = link_to admin_labels_path, title: 'Labels' do + %span + Labels + + = nav_link(controller: :appearances) do + = link_to admin_appearances_path, title: 'Appearances' do + %span + Appearance + + %li.divider + = nav_link(controller: :application_settings) do + = link_to admin_application_settings_path, title: 'Settings' do + %span + Settings diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml new file mode 100644 index 00000000000..7109baa4dad --- /dev/null +++ b/app/views/layouts/nav/_new_dashboard.html.haml @@ -0,0 +1,33 @@ +%ul.list-unstyled.navbar-sub-nav + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "home"}) do + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + Projects + + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do + Groups + + = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + Activity + + %li.dropdown + %a{ href: "#", data: { toggle: "dropdown" } } + More + = icon("chevron-down", class: "dropdown-chevron") + .dropdown-menu + %ul + = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do + = link_to activity_dashboard_path, title: 'Activity' do + Activity + + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do + Milestones + + = nav_link(controller: 'dashboard/snippets') do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do + Snippets + %li.divider + %li + = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml new file mode 100644 index 00000000000..40385f251e3 --- /dev/null +++ b/app/views/layouts/nav/_new_explore.html.haml @@ -0,0 +1,19 @@ +%ul.list-unstyled.navbar-sub-nav + = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do + = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + Projects + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do + = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do + Groups + %li.dropdown + %a{ href: "#", data: { toggle: "dropdown" } } + More + = icon("chevron-down", class: "dropdown-chevron") + .dropdown-menu + %ul + = nav_link(controller: :snippets) do + = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do + Snippets + %li.divider + %li + = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml new file mode 100644 index 00000000000..3b658e055b3 --- /dev/null +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -0,0 +1,56 @@ +.nav-sidebar + %ul.sidebar-top-level-items + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Home' do + %span + Group + + %ul.sidebar-sub-level-items + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group Home' do + %span + Home + + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity + + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do + = link_to issues_group_path(@group), title: 'Issues' do + %span + Issues + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + %span.badge.count= number_with_delimiter(issues.count) + + %ul.sidebar-sub-level-items + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do + %span + List + + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do + %span + Labels + + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do + %span + Milestones + + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + %span + Merge Requests + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + %span + Members + - if current_user && can?(current_user, :admin_group, @group) + = nav_link(path: %w[groups#projects groups#edit]) do + = link_to edit_group_path(@group), title: 'Settings' do + %span + Settings diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml new file mode 100644 index 00000000000..37ffbbecca8 --- /dev/null +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -0,0 +1,49 @@ +.nav-sidebar + %ul.sidebar-top-level-items + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + = link_to profile_path, title: 'Profile Settings' do + %span + Profile + = nav_link(controller: [:accounts, :two_factor_auths]) do + = link_to profile_account_path, title: 'Account' do + %span + Account + - if current_application_settings.user_oauth_applications? + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path, title: 'Applications' do + %span + Applications + = nav_link(controller: :chat_names) do + = link_to profile_chat_names_path, title: 'Chat' do + %span + Chat + = nav_link(controller: :personal_access_tokens) do + = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do + %span + Access Tokens + = nav_link(controller: :emails) do + = link_to profile_emails_path, title: 'Emails' do + %span + Emails + - unless current_user.ldap_user? + = nav_link(controller: :passwords) do + = link_to edit_profile_password_path, title: 'Password' do + %span + Password + = nav_link(controller: :notifications) do + = link_to profile_notifications_path, title: 'Notifications' do + %span + Notifications + + = nav_link(controller: :keys) do + = link_to profile_keys_path, title: 'SSH Keys' do + %span + SSH Keys + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences' do + %span + Preferences + = nav_link(path: 'profiles#audit_log') do + = link_to audit_log_profile_path, title: 'Authentication log' do + %span + Authentication log diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml new file mode 100644 index 00000000000..f85781737aa --- /dev/null +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -0,0 +1,242 @@ +.nav-sidebar + - can_edit = can?(current_user, :admin_project, @project) + %ul.sidebar-top-level-items + = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + %span + Project + + %ul.sidebar-sub-level-items + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: _('Project home'), class: 'shortcuts-project' do + %span= _('Home') + + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + %span= _('Activity') + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(path: 'cycle_analytics#show') do + = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do + %span= _('Cycle Analytics') + + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do + = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do + %span + Repository + + %ul.sidebar-sub-level-items + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + #{ _('Files') } + + = nav_link(controller: [:commit, :commits]) do + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{ _('Commits') } + + = nav_link(html_options: {class: branches_tab_class}) do + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{ _('Branches') } + + = nav_link(controller: [:tags, :releases]) do + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{ _('Tags') } + + = nav_link(path: 'graphs#show') do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref) do + #{ _('Contributors') } + + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + #{ s_('ProjectNetworkGraph|Graph') } + + = nav_link(controller: :compare) do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do + #{ _('Compare') } + + = nav_link(path: 'graphs#charts') do + = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref) do + #{ _('Charts') } + + - if project_nav_tab? :container_registry + = nav_link(controller: %w[projects/registry/repositories]) do + = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + %span + Registry + + - if project_nav_tab? :issues + = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do + %span + Issues + - if @project.default_issues_tracker? + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + + %ul.sidebar-sub-level-items + - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) + = nav_link(controller: :issues) do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do + %span + List + + = nav_link(controller: :boards) do + = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do + %span + Board + + - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do + %span + Merge Requests + + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %span + Labels + + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %span + Milestones + + - if project_nav_tab? :merge_requests + = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + %span + Merge Requests + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + + - if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + + %ul.sidebar-sub-level-items + - if project_nav_tab? :pipelines + = nav_link(path: ['pipelines#index', 'pipelines#show']) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + + - if project_nav_tab? :builds + = nav_link(controller: [:jobs, :artifacts]) do + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + %span + Jobs + + - if project_nav_tab? :pipelines + = nav_link(controller: :pipeline_schedules) do + = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + %span + Schedules + + - if project_nav_tab? :environments + = nav_link(controller: :environments) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments + + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_namespace_project_pipelines_path(@project.namespace, @project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + %span + Charts + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + %span + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do + %span + Snippets + + - if project_nav_tab? :settings + = nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do + = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + %span + Settings + + %ul.sidebar-sub-level-items + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(controller: :projects) do + = link_to edit_project_path(@project), title: 'General' do + %span + General + = nav_link(controller: :members) do + = link_to project_settings_members_path(@project), title: 'Members' do + %span + Members + - if can_edit + = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do + = link_to project_settings_integrations_path(@project), title: 'Integrations' do + %span + Integrations + = nav_link(controller: :repository) do + = link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do + %span + Repository + - if @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'Pipelines' do + %span + Pipelines + - if Gitlab.config.pages.enabled + = nav_link(controller: :pages) do + = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do + %span + Pages + + - else + = nav_link(path: %w[members#show]) do + = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Settings', class: 'shortcuts-tree' do + %span + Settings + + -# Shortcut to Project > Activity + %li.hidden + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + -# Shortcut to Repository > Graph (formerly, Network) + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Graph + + -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") + - unless @project.empty_repo? + %li.hidden + = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do + Charts + + -# Shortcut to Issues > New Issue + %li.hidden + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do + Create a new issue + + -# Shortcut to Pipelines > Jobs + - if project_nav_tab? :builds + %li.hidden + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs + + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + + -# Shortcut to issue boards + %li.hidden + = link_to 'Issue Boards', namespace_project_boards_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 0ee8a57dbd4..c365839e605 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,6 +1,10 @@ - page_title "User Settings" - header_title "User Settings", profile_path unless header_title - sidebar "dashboard" -- nav "profile" +- if show_new_nav? + - nav "new_profile_sidebar" + - @new_sidebar = true +- else + - nav "profile" = render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 3f5b0c54e50..4458c3c2c23 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,7 +1,11 @@ - page_title @project.name_with_namespace - page_description @project.description unless page_description - header_title project_title(@project) unless header_title -- nav "project" +- if show_new_nav? + - nav "new_project_sidebar" + - @new_sidebar = true +- else + - nav "project" - content_for :project_javascripts do - project = @target_project || @project diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index a83faa839df..b7a60938132 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 9c2e2a599b2..3f16885b8e3 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 0ff19b3eab1..0b5995415e9 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -15,6 +15,25 @@ .preview= image_tag "#{scheme.css_class}-scheme-preview.png" = f.radio_button :color_scheme_id, scheme.id = scheme.name + - if can_toggle_new_nav? + .col-sm-12 + %hr + .col-lg-3.profile-settings-sidebar#new-navigation + %h4.prepend-top-0 + New Navigation + %p + This setting allows you to turn on or off the new upcoming navigation concept. + = succeed '.' do + = link_to 'Learn more', '', target: '_blank' + .col-lg-9.syntax-theme + = label_tag do + .preview= image_tag "old_nav.png" + %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? } + Old + = label_tag do + .preview= image_tag "new_nav.png" + %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? } + New .col-sm-12 %hr .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index fcfd350f0da..819c98946ab 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,6 +1,6 @@ = render 'profiles/head' -= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| += bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f| = form_errors(@user) .row @@ -11,11 +11,11 @@ - if @user.avatar? You can change your avatar here - if gravatar_enabled? - or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} + or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} - else You can upload an avatar here - if gravatar_enabled? - or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} + or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} .col-lg-9 .clearfix.avatar-image.append-bottom-default = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do @@ -26,12 +26,12 @@ %a.btn.js-choose-user-avatar-button Browse file... %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen - = f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*" + = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*' .help-block The maximum file size allowed is 200KB. - if @user.avatar? %hr - = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?" }, method: :delete, class: "btn btn-gray" + = link_to 'Remove avatar', profile_avatar_path, data: { confirm: 'Avatar will be removed. Are you sure?' }, method: :delete, class: 'btn btn-gray' %hr .row .col-lg-3.profile-settings-sidebar @@ -42,85 +42,51 @@ - if current_user.ldap_user? Some options are unavailable for LDAP accounts .col-lg-9 - .form-group - = f.label :name, class: "label-light" - = f.text_field :name, class: "form-control", required: true - %span.help-block Enter your name, so people you know can recognize you. + .row + = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, + help: 'Enter your name, so people you know can recognize you.' + = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } - .form-group - = f.label :email, class: "label-light" - - if @user.external_email? - = f.text_field :email, class: "form-control", required: true, readonly: true - %span.help-block.light - Your email address was automatically set based on your #{email_provider_label} account. - - else - - if @user.temp_oauth_email? - = f.text_field :email, class: "form-control", required: true, value: nil - - else - = f.text_field :email, class: "form-control", required: true - - if @user.unconfirmed_email.present? - %span.help-block - Please click the link in the confirmation email before continuing. It was sent to - = succeed "." do - %strong= @user.unconfirmed_email - %p - = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post - - - else - %span.help-block We also use email for avatar detection if no avatar is uploaded. - .form-group - = f.label :public_email, class: "label-light" - = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2" - %span.help-block This email will be displayed on your public profile. - .form-group - = f.label :preferred_language, class: "label-light" - = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, - {}, class: "select2" - %span.help-block This feature is experimental and translations are not complete yet. - .form-group - = f.label :skype, class: "label-light" - = f.text_field :skype, class: "form-control" - .form-group - = f.label :linkedin, class: "label-light" - = f.text_field :linkedin, class: "form-control" - .form-group - = f.label :twitter, class: "label-light" - = f.text_field :twitter, class: "form-control" - .form-group - = f.label :website_url, 'Website', class: "label-light" - = f.text_field :website_url, class: "form-control" - .form-group - = f.label :location, 'Location', class: "label-light" - = f.text_field :location, class: "form-control" - .form-group - = f.label :organization, 'Organization', class: "label-light" - = f.text_field :organization, class: "form-control" - .form-group - = f.label :bio, class: "label-light" - = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250 - %span.help-block Tell us about yourself in fewer than 250 characters. + - if @user.external_email? + = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{email_provider_label} account." + - else + = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?), + help: user_email_help_text(@user) + = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), + { help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' }, + control_class: 'select2' + = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, + { help: 'This feature is experimental and translations are not complete yet.' }, + control_class: 'select2' + = f.text_field :skype + = f.text_field :linkedin + = f.text_field :twitter + = f.text_field :website_url, label: 'Website' + = f.text_field :location + = f.text_field :organization + = f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.' .prepend-top-default.append-bottom-default - = f.submit 'Update profile settings', class: "btn btn-success" - = link_to "Cancel", user_path(current_user), class: "btn btn-cancel" + = f.submit 'Update profile settings', class: 'btn btn-success' + = link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel' .modal.modal-profile-crop .modal-dialog .modal-content .modal-header - %button.close{ :type => "button", :'data-dismiss' => "modal" } + %button.close{ type: 'button', 'data-dismiss': 'modal' } %span × %h4.modal-title Position and size your new avatar .modal-body .profile-crop-image-container - %img.modal-profile-crop-image{ alt: "Avatar cropper" } + %img.modal-profile-crop-image{ alt: 'Avatar cropper' } .crop-controls .btn-group - %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } } + %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } } %span.fa.fa-search-plus - %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } } + %button.btn.btn-primary{ data: { method: 'zoom', option: '-0.1' } } %span.fa.fa-search-minus .modal-footer - %button.btn.btn-primary.js-upload-user-avatar{ :type => "button" } + %button.btn.btn-primary.js-upload-user-avatar{ type: 'button' } Set new profile picture diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml index c748ccf65e6..cb4d2bbacf5 100644 --- a/app/views/projects/_find_file_link.html.haml +++ b/app/views/projects/_find_file_link.html.haml @@ -1,3 +1,3 @@ -= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do += link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn shortcuts-find-file', rel: 'nofollow' do = icon('search') %span= _('Find file') diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 07445434cf3..d0698285f84 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -9,6 +9,12 @@ %li %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview + + - if defined?(@issue) && @issue.confidential? + %li.confidential-issue-warning + = icon('warning') + %span This is a confidential issue. Your comment will not be visible to the public. + %li.pull-right .toolbar-group = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml index 65fc0a36ca9..4026b9e3c46 100644 --- a/app/views/projects/_visibility_select.html.haml +++ b/app/views/projects/_visibility_select.html.haml @@ -1,5 +1,7 @@ - if can_change_visibility_level?(@project, current_user) - = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select') + .select-wrapper + = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control') + = icon('chevron-down') - else .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } } = visibility_level_icon(@project.visibility_level) diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 3b3d08ddd3c..afc40ca4eab 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,10 +1,15 @@ - @gfm_form = true - current_text ||= nil -- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) +- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) +- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands } + = f.text_area attr, + class: classes, + placeholder: placeholder, + data: { supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete } - else = text_area_tag attr, current_text, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } diff --git a/app/views/projects/blame/_age_map_legend.html.haml b/app/views/projects/blame/_age_map_legend.html.haml new file mode 100644 index 00000000000..533dc20ffb3 --- /dev/null +++ b/app/views/projects/blame/_age_map_legend.html.haml @@ -0,0 +1,12 @@ +%span.left-label Newer +%span.legend-box.legend-box-0 +%span.legend-box.legend-box-1 +%span.legend-box.legend-box-2 +%span.legend-box.legend-box-3 +%span.legend-box.legend-box-4 +%span.legend-box.legend-box-5 +%span.legend-box.legend-box-6 +%span.legend-box.legend-box-7 +%span.legend-box.legend-box-8 +%span.legend-box.legend-box-9 +%span.right-label Older diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index a6ee2b2f7b8..3627f72f5e1 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,5 +1,6 @@ - @no_container = true -- page_title "Annotate", @blob.path, @ref +- project_duration = age_map_duration(@blame_groups, @project) +- page_title "Blame", @blob.path, @ref = render "projects/commits/head" %div{ class: container_class } @@ -8,15 +9,16 @@ .file-holder = render "projects/blob/header", blob: @blob, blame: true - + .file-blame-legend + = render 'age_map_legend' .table-responsive.file-content.blame.code.js-syntax-highlight %table - current_line = 1 - @blame_groups.each do |blame_group| %tr - %td.blame-commit + - commit = blame_group[:commit] + %td.blame-commit{ class: age_map_class(commit.committed_date, project_duration) } .commit - - commit = blame_group[:commit] = author_avatar(commit, size: 36) .commit-row-title %strong diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml index 0ad9f258e48..2c944b516a4 100644 --- a/app/views/projects/blob/_breadcrumb.html.haml +++ b/app/views/projects/blob/_breadcrumb.html.haml @@ -1,16 +1,33 @@ - blame = local_assigns.fetch(:blame, false) .nav-block + .tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'blob', path: @path + + %ul.breadcrumb.repo-breadcrumb + %li + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + - path_breadcrumbs do |title, path| + - title = truncate(title, length: 40) + %li + - if path == @path + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do + %strong= title + - else + = link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) + .tree-controls = render 'projects/find_file_link' - .btn-group.prepend-left-10{ role: "group" }< + .btn-group{ role: "group" }< -# only show normal/blame view links for text files - if blob.readable_text? - if blame = link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn' - else - = link_to 'Annotate', namespace_project_blame_path(@project.namespace, @project, @id), + = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), class: 'btn js-blob-blame-link' unless blob.empty? = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), @@ -18,19 +35,3 @@ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url' - - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'blob', path: @path - - %ul.breadcrumb.repo-breadcrumb - %li - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - path_breadcrumbs do |title, path| - - title = truncate(title, length: 40) - %li - - if path == @path - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do - %strong= title - - else - = link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index e14885f264b..32dbc1b3417 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -9,8 +9,10 @@ .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light - Attach a file by drag & drop or - = link_to 'click to upload', '#', class: "markdown-selector" + - upload_link = link_to s_('UploadLink|click to upload'), '#', class: "markdown-selector" + - dropzone_text = _('Attach a file by drag & drop or %{upload_link}') % { upload_link: upload_link } + #{ dropzone_text.html_safe } + %br .dropzone-alerts.alert.alert-danger.data{ style: "display:none" } @@ -18,7 +20,7 @@ .form-actions = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index 55c4d51be14..539ee087b14 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -9,11 +9,11 @@ %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" } } {{ list.title }} - .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } - %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } + .issue-count-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } + %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } {{ list.issuesSize }} - if can?(current_user, :admin_issue, @project) - %button.btn.btn-small.btn-default.pull-right.has-tooltip.js-no-trigger-collapse{ type: "button", + %button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button", "@click" => "showNewIssueForm", "v-if" => 'list.type !== "closed"', "aria-label" => "New issue", diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 3cf91bf07f7..a73ddd5eb33 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -2,7 +2,7 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) .project-action-button.dropdown.inline> - %button.btn.has-tooltip{ title: 'Download', 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } + %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } = icon('download') = icon("caret-down") %span.sr-only= _('Select Archive Format') diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 312c349da3a..960b57a8008 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,6 +1,6 @@ - if current_user .project-action-button.dropdown.inline - %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: 'Create new...', 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => 'Create new...' } + %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 7a08bb37494..42f8c75f57b 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -4,11 +4,15 @@ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do = custom_icon('icon_fork') %span= s_('GoToYourFork|Fork') + - elsif !current_user.can_create_project? + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: _('You have reached your project limit'), class: 'btn has-tooltip disabled' do + = custom_icon('icon_fork') + %span= s_('CreateNewFork|Fork') - else = link_to new_namespace_project_fork_path(@project.namespace, @project), class: 'btn' do = custom_icon('icon_fork') %span= s_('CreateNewFork|Fork') .count-with-arrow %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), title: n_('Forks', @project.forks_count), class: 'count' do + = link_to namespace_project_forks_path(@project.namespace, @project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do = @project.forks_count diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 281d823da52..2267f123e38 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -1,35 +1,36 @@ - case type.to_s - when 'revert' - - label = 'Revert' - - branch_label = 'Revert in branch' + - label = s_('ChangeTypeAction|Revert') + - branch_label = s_('ChangeTypeActionLabel|Revert in branch') + - revert_merge_request = _('Revert this merge request') + - revert_commit = _('Revert this commit') + - title = commit.merged_merge_request(current_user) ? revert_merge_request : revert_commit - when 'cherry-pick' - - label = 'Cherry-pick' - - branch_label = 'Pick into branch' + - label = s_('ChangeTypeAction|Cherry-pick') + - branch_label = s_('ChangeTypeActionLabel|Pick into branch') + - title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit') .modal{ id: "modal-#{type}-commit" } .modal-dialog .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3.page-title== #{label} this #{commit.change_type_title(current_user)} + %h3.page-title= title .modal-body = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'start_branch', branch_label, class: 'control-label' .col-sm-10 = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' - = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) + = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) - .checkbox - = label_tag do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil - Start a <strong>new merge request</strong> with these changes + = render 'shared/new_merge_request_checkbox' - else = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index aab50310234..7fe44816bae 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,17 +1,17 @@ .page-content-header .header-main-content %strong - Commit + #{ s_('CommitBoxTitle|Commit') } %span.commit-sha= @commit.short_id - = clipboard_button(text: @commit.id, title: "Copy commit SHA to clipboard") + = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard")) %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} - %span by + %span= s_('ByAuthor|by') = author_avatar(@commit, size: 24) %strong = commit_author_link(@commit, avatar: true, size: 24) - if @commit.different_committer? - %span.light Committed by + %span.light= _('Committed by') %strong = commit_committer_link(@commit, avatar: true, size: 24) #{time_ago_with_tooltip(@commit.committed_date)} @@ -22,15 +22,15 @@ = icon('comment') = @notes_count = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do - Browse files + #{ _('Browse files') } .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } - %span Options + %span= _('Options') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li.visible-xs-block.visible-sm-block = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do - Browse Files + _('Browse Files') - unless @commit.has_been_reverted?(current_user) %li.clearfix = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) @@ -38,13 +38,13 @@ = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) - if can_collaborate_with_project? %li.clearfix - = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) + = link_to s_("CreateTag|Tag"), new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.divider %li.dropdown-header - Download + #{ _('Download') } - unless @commit.parents.length > 1 - %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) - %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) + %li= link_to s_("DownloadCommit|Email Patches"), namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) + %li= link_to s_("DownloadCommit|Plain Diff"), namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) .commit-box %h3.commit-title @@ -57,7 +57,7 @@ .well-segment.branch-info .icon-container.commit-icon = custom_icon("icon_commit") - %span.cgray= pluralize(@commit.parents.count, "parent") + %span.cgray= n_('parent', 'parents', @commit.parents.count) - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "commit-sha" %span.commit-info.branches @@ -69,11 +69,11 @@ .status-icon-container{ class: "ci-status-icon-#{@commit.status}" } = link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do = ci_icon_for_status(last_pipeline.status) - Pipeline + #{ _('Pipeline') } = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) = ci_label_for_status(last_pipeline.status) - if last_pipeline.stages_count.nonzero? - with #{"stage".pluralize(last_pipeline.stages_count)} + #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } .mr-widget-pipeline-graph = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' in diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 3a1be3fa4b6..b778e8af121 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : '' -- limited_container_width = fluid_layout || diff_view == :inline ? '' : 'limit-container-width' +- limited_container_width = fluid_layout ? '' : 'limit-container-width' - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description = render "projects/commits/head" @@ -13,7 +13,8 @@ .block-connector = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - = render "shared/notes/notes_with_form", :autocomplete => true - - if can_collaborate_with_project? - - %w(revert cherry-pick).each do |type| - = render "projects/commit/change", type: type, commit: @commit, title: @commit.title + .limited-width-notes + = render "shared/notes/notes_with_form", :autocomplete => true + - if can_collaborate_with_project? + - %w(revert cherry-pick).each do |type| + = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 7a03c3561af..8a4ef5a45b3 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -5,7 +5,7 @@ - notes = commit.notes - note_count = notes.user.count -- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] +- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits)] - cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do @@ -30,9 +30,11 @@ %pre.commit-row-description.js-toggle-content = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) .commiter - = commit_author_link(commit, avatar: false, size: 24) - #{ _('committed') } - #{time_ago_with_tooltip(commit.committed_date)} + - commit_author_link = commit_author_link(commit, avatar: false, size: 24) + - commit_timeago = time_ago_with_tooltip(commit.committed_date) + - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } + #{ commit_text.html_safe } + .commit-actions.flex-row.hidden-xs - if commit.status(ref) diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index d3380c917e4..93fd0789c11 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,8 +3,8 @@ - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header.js-commit-header{ data: { day: day } } - %span.day= day.strftime('%d %b, %Y') - %span.commits-count= pluralize(commits.count, 'commit') + %span.day= l(day, format: '%d %b, %Y') + %span.commits-count= n_("%d commit", "%d commits", commits.count) % commits.count %li.commits-row{ data: { day: day } } %ul.content-list.commit-list @@ -12,4 +12,4 @@ - if hidden > 0 %li.alert.alert-warning - #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. + = n_('%d additional commit has been omitted to prevent performance issues.', '%d additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index c1c2fb3d299..fabd825aec8 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true -- page_title "Commits", @ref +- page_title _("Commits"), @ref = content_for :meta_tags do = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") @@ -18,16 +18,16 @@ .block-controls.hidden-xs.hidden-sm - if @merge_request.present? .control - = link_to "View open merge request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' + = link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' - elsif create_mr_button?(@repository.root_ref, @ref) .control - = link_to "Create merge request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' + = link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' .control = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do - = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } + = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits feed", class: 'btn' do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do = icon("rss") %div{ id: dom_id(@project) } diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 6e038ffd9c0..cb98ce04430 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -4,7 +4,7 @@ %h4 Deploy Keys %button.btn.js-settings-toggle - = expanded ? 'Close' : 'Expand' + = expanded ? 'Collapse' : 'Expand' %p Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. .settings-content.no-animate{ class: ('expanded' if expanded) } diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index d956cb2cc1a..520696b01c6 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -3,24 +3,28 @@ .table-mobile-header{ role: 'rowheader' } ID %strong.table-mobile-content ##{deployment.iid} - .table-section.section-40{ role: 'gridcell' } + .table-section.section-30{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' } Commit = render 'projects/deployments/commit', deployment: deployment - .table-section.section-15.build-column{ role: 'gridcell' } + .table-section.section-25.build-column{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' } Job - if deployment.deployable - = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link table-mobile-content' do - #{deployment.deployable.name} (##{deployment.deployable.id}) - - if deployment.user - by - = user_avatar(user: deployment.user, size: 20) + .table-mobile-content + .flex-truncate-parent + .flex-truncate-child + = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do + #{deployment.deployable.name} (##{deployment.deployable.id}) + - if deployment.user + %div + by + = user_avatar(user: deployment.user, size: 20) .table-section.section-15{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' } Created %span.table-mobile-content= time_ago_with_tooltip(deployment.created_at) - .table-section.section-20.environments-actions.table-button-footer{ role: 'gridcell' } - .btn-group.environment-action-buttons + .table-section.section-20.table-button-footer{ role: 'gridcell' } + .btn-group.table-action-buttons = render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml new file mode 100644 index 00000000000..8772bd4705f --- /dev/null +++ b/app/views/projects/diffs/_collapsed.html.haml @@ -0,0 +1,5 @@ +- diff_file = viewer.diff_file +- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) +.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } + This diff is collapsed. + %a.click-to-expand Click to expand it. diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index ec1c434a4b8..68f74f702ea 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -1,27 +1,2 @@ -- blob = diff_file.blob - .diff-content - - if diff_file.too_large? - .nothing-here-block This diff could not be displayed because it is too large. - - elsif blob.truncated? - .nothing-here-block The file could not be displayed because it is too large. - - elsif blob.readable_text? - - if !diff_file.diffable? - .nothing-here-block This diff was suppressed by a .gitattributes entry. - - elsif diff_file.collapsed? - - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) - .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } - This diff is collapsed. - %a.click-to-expand - Click to expand it. - - elsif diff_file.diff_lines.length > 0 - = render "projects/diffs/viewers/text", diff_file: diff_file - - else - - if diff_file.mode_changed? - .nothing-here-block File mode changed - - elsif diff_file.renamed_file? - .nothing-here-block File moved - - elsif blob.image? - = render "projects/diffs/viewers/image", diff_file: diff_file - - else - .nothing-here-block No preview for this file type + = render 'projects/diffs/viewer', viewer: diff_file.rich_viewer || diff_file.simple_viewer diff --git a/app/views/projects/diffs/_render_error.html.haml b/app/views/projects/diffs/_render_error.html.haml new file mode 100644 index 00000000000..47a9ac3ee6b --- /dev/null +++ b/app/views/projects/diffs/_render_error.html.haml @@ -0,0 +1,6 @@ +.nothing-here-block + This #{viewer.switcher_title} could not be displayed because #{diff_render_error_reason(viewer)}. + + You can + = diff_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe + instead. diff --git a/app/views/projects/diffs/_viewer.html.haml b/app/views/projects/diffs/_viewer.html.haml new file mode 100644 index 00000000000..5c4d1760871 --- /dev/null +++ b/app/views/projects/diffs/_viewer.html.haml @@ -0,0 +1,16 @@ +- hidden = local_assigns.fetch(:hidden, false) + +.diff-viewer{ data: { type: viewer.type }, class: ('hidden' if hidden) } + - if viewer.render_error + = render 'projects/diffs/render_error', viewer: viewer + - elsif viewer.collapsed? + = render 'projects/diffs/collapsed', viewer: viewer + - else + - viewer.prepare! + + -# In the rare case where the first kilobyte of the file looks like text, + -# but the file turns out to actually be binary after loading all data, + -# we fall back on the binary No Preview viewer. + - viewer = DiffViewer::NoPreview.new(viewer.diff_file) if viewer.binary_detected_after_load? + + = render viewer.partial_path, viewer: viewer diff --git a/app/views/projects/diffs/viewers/_added.html.haml b/app/views/projects/diffs/viewers/_added.html.haml new file mode 100644 index 00000000000..8004fe16688 --- /dev/null +++ b/app/views/projects/diffs/viewers/_added.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + File added diff --git a/app/views/projects/diffs/viewers/_deleted.html.haml b/app/views/projects/diffs/viewers/_deleted.html.haml new file mode 100644 index 00000000000..0ac7b4ca8f6 --- /dev/null +++ b/app/views/projects/diffs/viewers/_deleted.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + File deleted diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml index ea75373581e..19d08181223 100644 --- a/app/views/projects/diffs/viewers/_image.html.haml +++ b/app/views/projects/diffs/viewers/_image.html.haml @@ -1,3 +1,4 @@ +- diff_file = viewer.diff_file - blob = diff_file.blob - old_blob = diff_file.old_blob - blob_raw_path = diff_file_blob_raw_path(diff_file) diff --git a/app/views/projects/diffs/viewers/_mode_changed.html.haml b/app/views/projects/diffs/viewers/_mode_changed.html.haml new file mode 100644 index 00000000000..69bc96bbdad --- /dev/null +++ b/app/views/projects/diffs/viewers/_mode_changed.html.haml @@ -0,0 +1,3 @@ +- diff_file = viewer.diff_file +.nothing-here-block + File mode changed from #{diff_file.a_mode} to #{diff_file.b_mode} diff --git a/app/views/projects/diffs/viewers/_no_preview.html.haml b/app/views/projects/diffs/viewers/_no_preview.html.haml new file mode 100644 index 00000000000..befe070af2b --- /dev/null +++ b/app/views/projects/diffs/viewers/_no_preview.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + No preview for this file type diff --git a/app/views/projects/diffs/viewers/_not_diffable.html.haml b/app/views/projects/diffs/viewers/_not_diffable.html.haml new file mode 100644 index 00000000000..b2c677ec59c --- /dev/null +++ b/app/views/projects/diffs/viewers/_not_diffable.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + This diff was suppressed by a .gitattributes entry. diff --git a/app/views/projects/diffs/viewers/_renamed.html.haml b/app/views/projects/diffs/viewers/_renamed.html.haml new file mode 100644 index 00000000000..ef05ee38d8d --- /dev/null +++ b/app/views/projects/diffs/viewers/_renamed.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + File moved diff --git a/app/views/projects/diffs/viewers/_text.html.haml b/app/views/projects/diffs/viewers/_text.html.haml index 120d3540223..509e68598c9 100644 --- a/app/views/projects/diffs/viewers/_text.html.haml +++ b/app/views/projects/diffs/viewers/_text.html.haml @@ -1,5 +1,5 @@ +- diff_file = viewer.diff_file - blob = diff_file.blob -- blob.load_all_data! - total_lines = blob.lines.size - total_lines -= 1 if total_lines > 0 && blob.lines.last.blank? - if diff_view == :parallel diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c3dab68cea5..78057facde7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,10 +1,12 @@ +- @content_class = "limit-container-width" unless fluid_layout + = render "projects/settings/head" .project-edit-container .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Project settings - .col-lg-9 + .col-lg-8 .project-edit-errors = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| %fieldset @@ -39,69 +41,69 @@ Sharing & Permissions .form_group.prepend-top-20.sharing-and-permissions .row.js-visibility-select - .col-md-9 + .col-md-8 .label-light = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level = link_to icon('question-circle'), help_page_path("public_access/public_access") %span.help-block - .col-md-3.visibility-select-container + .col-md-4.visibility-select-container = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) = f.fields_for :project_feature do |feature_fields| %fieldset.features .row - .col-md-9.project-feature + .col-md-8.project-feature = feature_fields.label :repository_access_level, "Repository", class: 'label-light' %span.help-block View and edit files in this project - .col-md-3.js-repo-access-level + .col-md-4.js-repo-access-level = project_feature_access_select(:repository_access_level) .row - .col-md-9.project-feature.nested + .col-md-8.project-feature.nested = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' %span.help-block Submit changes to be merged upstream - .col-md-3 + .col-md-4 = project_feature_access_select(:merge_requests_access_level) .row - .col-md-9.project-feature.nested + .col-md-8.project-feature.nested = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' %span.help-block Build, test, and deploy your changes - .col-md-3 + .col-md-4 = project_feature_access_select(:builds_access_level) .row - .col-md-9.project-feature + .col-md-8.project-feature = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' %span.help-block Share code pastes with others out of Git repository - .col-md-3 + .col-md-4 = project_feature_access_select(:snippets_access_level) .row - .col-md-9.project-feature + .col-md-8.project-feature = feature_fields.label :issues_access_level, "Issues", class: 'label-light' %span.help-block Lightweight issue tracking system for this project - .col-md-3 + .col-md-4 = project_feature_access_select(:issues_access_level) .row - .col-md-9.project-feature + .col-md-8.project-feature = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' %span.help-block Pages for project documentation - .col-md-3 + .col-md-4 = project_feature_access_select(:wiki_access_level) .form-group = render 'shared/allow_request_access', form: f - if Gitlab.config.lfs.enabled && current_user.admin? .row.js-lfs-enabled - .col-md-9 + .col-md-8 = f.label :lfs_enabled, 'LFS', class: 'label-light' %span.help-block Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' } - - + .col-md-4 + .select-wrapper + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } + = icon('chevron-down') - if Gitlab.config.registry.enabled .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } .checkbox @@ -138,19 +140,19 @@ .row.prepend-top-default %hr .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0 Housekeeping %p.append-bottom-0 %p Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects. - .col-lg-9 + .col-lg-8 = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-default" %hr .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0 Export project %p.append-bottom-0 @@ -159,7 +161,7 @@ %p Once the exported file is ready, you will receive a notification email with a download link. - .col-lg-9 + .col-lg-8 - if @project.export_project_path = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), @@ -190,7 +192,7 @@ - if can? current_user, :archive_project, @project %hr .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.warning-title.prepend-top-0 - if @project.archived? Unarchive project @@ -201,7 +203,7 @@ Unarchiving the project will mark its repository as active. The project can be committed to. - else Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. - .col-lg-9 + .col-lg-8 - if @project.archived? %p %strong Once active this project shows up in the search and on the dashboard. @@ -216,10 +218,10 @@ method: :post, class: "btn btn-warning" %hr .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0.warning-title Rename repository - .col-lg-9 + .col-lg-8 = render 'projects/errors' = form_for([@project.namespace.becomes(Namespace), @project]) do |f| .form-group.project_name_holder @@ -244,12 +246,12 @@ - if can?(current_user, :change_namespace, @project) %hr .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0.danger-title Transfer project to new group %p.append-bottom-0 Please select the group you want to transfer this project to in the dropdown to the right. - .col-lg-9 + .col-lg-8 = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| .form-group = label_tag :new_namespace_id, nil, class: 'label-light' do @@ -265,7 +267,7 @@ - if @project.forked? && can?(current_user, :remove_fork_project, @project) %hr .row.prepend-top-default.append-bottom-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0.danger-title Remove fork relationship %p.append-bottom-0 @@ -273,7 +275,7 @@ This will remove the fork relationship to source project = succeed "." do = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) - .col-lg-9 + .col-lg-8 = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| %p %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. @@ -281,12 +283,12 @@ - if can?(current_user, :remove_project, @project) %hr .row.prepend-top-default.append-bottom-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0.danger-title Remove project %p.append-bottom-0 Removing the project will delete its repository and all related resources including issues, merge requests etc. - .col-lg-9 + .col-lg-8 = form_tag(namespace_project_path(@project.namespace, @project), method: :delete) do %p %strong Removed projects cannot be restored! diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 23aa4c29e69..31e2bb11ce8 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -31,8 +31,8 @@ .ci-table.environments{ role: 'grid' } .gl-responsive-table-row.table-row-header{ role: 'row' } .table-section.section-10{ role: 'columnheader' } ID - .table-section.section-40{ role: 'columnheader' } Commit - .table-section.section-15{ role: 'columnheader' } Job + .table-section.section-30{ role: 'columnheader' } Commit + .table-section.section-25{ role: 'columnheader' } Job .table-section.section-15{ role: 'columnheader' } Created = render @deployments diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml index 676b7c345bc..776681ea09a 100644 --- a/app/views/projects/hooks/_index.html.haml +++ b/app/views/projects/hooks/_index.html.haml @@ -1,12 +1,12 @@ .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0 = page_title %p #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be used for binding events when something is happening within the project. - .col-lg-9.append-bottom-default + .col-lg-8.append-bottom-default = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f| = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } = f.submit 'Add webhook', class: 'btn btn-create' diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml index da65157a10b..35b7d1b920c 100644 --- a/app/views/projects/issues/_issue_by_email.html.haml +++ b/app/views/projects/issues/_issue_by_email.html.haml @@ -20,7 +20,7 @@ %p The subject will be used as the title of the new issue, and the message will be the description. - = link_to 'Slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 + = link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1 and styling with = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 are supported. diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index d48923b422a..bda52fe461c 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -14,7 +14,7 @@ = merge_request.to_reference %span.merge-request-info %strong - = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" + = link_to merge_request.title, merge_request_path(merge_request), class: "row_title" - unless @issue.project.id == merge_request.target_project.id in - project = merge_request.target_project diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5f92d020eef..d909b0bfbbd 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -5,13 +5,6 @@ - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) -- if defined?(@issue) && @issue.confidential? - .confidential-issue-warning{ data: { spy: 'affix' } } - %span.confidential-issue-text - #{confidential_icon(@issue)} This issue is confidential. - %a{ href: help_page_path('user/project/issues/confidential_issues'), target: '_blank' } - What are confidential issues? - .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } @@ -26,6 +19,7 @@ = icon('angle-double-left') .issuable-meta + = confidential_icon(@issue) = issuable_meta(@issue, @project, "Issue") .issuable-actions diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml index 62c9748c510..e675e1830d0 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -1,7 +1,7 @@ .form-horizontal.resolve-conflicts-form .form-group %label.col-sm-2.control-label{ "for" => "commit-message" } - Commit message + #{ _('Commit message') } .col-sm-10 .commit-message-container .max-width-marker diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index e0d45054854..75a4687e1e3 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -1,14 +1,19 @@ -.dropdown.more-actions - = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do - = icon('ellipsis-v', class: 'icon') - %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left - %li - = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' - %li.divider - %li - = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do - Report as abuse - - if note_editable - %li - = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do - %span.text-danger Delete comment +- is_current_user = current_user == note.author + +- if note_editable || !is_current_user + .dropdown.more-actions + = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do + = icon('ellipsis-v', class: 'icon') + %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left + - if note_editable + %li + = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' + %li.divider + - unless is_current_user + %li + = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do + Report as abuse + - if note_editable + %li + = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do + %span.text-danger Delete comment diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index e8dedf26206..fc7fa5c1876 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -7,7 +7,7 @@ .form-group .col-md-9 = f.label :description, _('Description'), class: 'label-light' - = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: _('PipelineSchedules|Provide a short description for this pipeline') + = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline') .form-group .col-md-9 = f.label :cron, _('Interval Pattern'), class: 'label-light' @@ -15,19 +15,19 @@ .form-group .col-md-9 = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light' - = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: _("Filter"), data: { data: timezone_data } } ) + = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true .form-group .col-md-9 = f.label :ref, _('Target Branch'), class: 'label-light' - = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: _("Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) + = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true .form-group .col-md-9 - = f.label :active, _('PipelineSchedules|Activated'), class: 'label-light' + = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' %div = f.check_box :active, required: false, value: @schedule.active? - Active + = _('Active') .footer-block.row-content-block = f.submit _('Save pipeline schedule'), class: 'btn btn-create', tabindex: 3 = link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 2d3344a4aaf..966d6cd8495 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -13,12 +13,12 @@ = ci_icon_for_status(pipeline_schedule.last_pipeline.status) %span ##{pipeline_schedule.last_pipeline.id} - else - = _("PipelineSchedules|None") + = s_("PipelineSchedules|None") %td.next-run-cell - if pipeline_schedule.active? = time_ago_with_tooltip(pipeline_schedule.real_next_run) - else - = _("PipelineSchedules|Inactive") + = s_("PipelineSchedules|Inactive") %td - if pipeline_schedule.owner = image_tag avatar_icon(pipeline_schedule.owner, 20), class: "avatar s20" diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 4a96ee652d2..c296152e54f 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -14,7 +14,7 @@ .nav-controls = link_to new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create' do - %span New schedule + %span= _('New schedule') - if @schedules.present? %ul.content-list diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index 4a5043aac3c..8ffddfe6154 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -15,7 +15,7 @@ .col-md-6 = render 'projects/pipelines/charts/overall' .col-md-6 - = render 'projects/pipelines/charts/build_times' + = render 'projects/pipelines/charts/pipeline_times' %hr - = render 'projects/pipelines/charts/builds' + = render 'projects/pipelines/charts/pipelines' diff --git a/app/views/projects/pipelines/charts/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml index 0b7e3d22dd7..93083397d5b 100644 --- a/app/views/projects/pipelines/charts/_overall.haml +++ b/app/views/projects/pipelines/charts/_overall.haml @@ -2,18 +2,14 @@ %ul %li Total: - %strong= pluralize @project.builds.count(:all), 'job' + %strong= pluralize @counts[:total], 'pipeline' %li Successful: - %strong= pluralize @project.builds.success.count(:all), 'job' + %strong= pluralize @counts[:success], 'pipeline' %li Failed: - %strong= pluralize @project.builds.failed.count(:all), 'job' + %strong= pluralize @counts[:failed], 'pipeline' %li Success ratio: %strong - #{success_ratio(@project.builds.success, @project.builds.failed)}% - %li - Commits covered: - %strong - = @project.pipelines.count(:all) + #{success_ratio(@counts)}% diff --git a/app/views/projects/pipelines/charts/_build_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml index bb0975a9535..aee7c5492aa 100644 --- a/app/views/projects/pipelines/charts/_build_times.haml +++ b/app/views/projects/pipelines/charts/_pipeline_times.haml @@ -6,7 +6,7 @@ :javascript var data = { - labels : #{@charts[:build_times].labels.to_json}, + labels : #{@charts[:pipeline_times].labels.to_json}, datasets : [ { fillColor : "rgba(220,220,220,0.5)", @@ -14,7 +14,7 @@ barStrokeWidth: 1, barValueSpacing: 1, barDatasetSpacing: 1, - data : #{@charts[:build_times].build_times.to_json} + data : #{@charts[:pipeline_times].pipeline_times.to_json} } ] } diff --git a/app/views/projects/pipelines/charts/_builds.haml b/app/views/projects/pipelines/charts/_pipelines.haml index b6f453b9736..b6f453b9736 100644 --- a/app/views/projects/pipelines/charts/_builds.haml +++ b/app/views/projects/pipelines/charts/_pipelines.haml diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml index 43bbd735059..3de518c8b9a 100644 --- a/app/views/projects/pipelines_settings/_badge.html.haml +++ b/app/views/projects/pipelines_settings/_badge.html.haml @@ -1,8 +1,8 @@ %div{ class: badge.title.gsub(' ', '-') } - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = badge.title.capitalize - .col-lg-9 + .col-lg-8 .prepend-top-10 .panel.panel-default .panel-heading diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 3b17daeb6da..580129ca809 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -1,8 +1,8 @@ .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Pipelines - .col-lg-9 + .col-lg-8 = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f| %fieldset.builds-feature - unless @repository.gitlab_ci_yml diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml index cfae371e169..fa99610c0be 100644 --- a/app/views/projects/project_members/_index.html.haml +++ b/app/views/projects/project_members/_index.html.haml @@ -1,5 +1,5 @@ .row.prepend-top-default - .col-lg-3.settings-sidebar + .col-lg-4.settings-sidebar %h4.prepend-top-0 Project members - if can?(current_user, :admin_project_member, @project) @@ -13,7 +13,7 @@ %i Masters or %i Owners - .col-lg-9 + .col-lg-8 .light - if can?(current_user, :admin_project_member, @project) %ul.nav-links.project-member-tabs{ role: 'tablist' } diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 247c4bdbe2d..8bf2246662a 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -6,7 +6,9 @@ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group = label_tag :access_level, "Choose a role permission", class: "label-light" - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .select-wrapper + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" + = icon('chevron-down') .help-block.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml index b7cc8dd7062..643569db646 100644 --- a/app/views/projects/project_members/_new_shared_group.html.haml +++ b/app/views/projects/project_members/_new_shared_group.html.haml @@ -8,7 +8,7 @@ = label_tag :link_group_access, "Max access level", class: "label-light" .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" - = icon('caret-down') + = icon('chevron-down') .help-block.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml index 9af67649741..5d2422bdf54 100644 --- a/app/views/projects/protected_branches/_index.html.haml +++ b/app/views/projects/protected_branches/_index.html.haml @@ -7,7 +7,7 @@ %h4 Protected Branches %button.btn.js-settings-toggle - = expanded ? 'Close' : 'Expand' + = expanded ? 'Collapse' : 'Expand' %p Keep stable branches secure and force developers to use merge requests. .settings-content.no-animate{ class: ('expanded' if expanded) } diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml index 976e1d7e93f..8250f692a69 100644 --- a/app/views/projects/protected_tags/_index.html.haml +++ b/app/views/projects/protected_tags/_index.html.haml @@ -7,7 +7,7 @@ %h4 Protected Tags %button.btn.js-settings-toggle - = expanded ? 'Close' : 'Expand' + = expanded ? 'Collapse' : 'Expand' %p Limit access to creating and updating tags. .settings-content.no-animate{ class: ('expanded' if expanded) } diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml index 8bc78f8d018..dcdc432b654 100644 --- a/app/views/projects/registry/repositories/_image.html.haml +++ b/app/views/projects/registry/repositories/_image.html.haml @@ -6,13 +6,14 @@ = clipboard_button(clipboard_text: "docker pull #{image.location}") - .controls.hidden-xs.pull-right - = link_to namespace_project_container_registry_path(@project.namespace, @project, image), - class: 'btn btn-remove has-tooltip', - title: 'Remove repository', - data: { confirm: 'Are you sure?' }, - method: :delete do - = icon('trash cred', 'aria-hidden': 'true') + - if can?(current_user, :update_container_image, @project) + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_path(@project.namespace, @project, image), + class: 'btn btn-remove has-tooltip', + title: 'Remove repository', + data: { confirm: 'Are you sure?' }, + method: :delete do + = icon('trash cred', 'aria-hidden': 'true') .container-image-tags.js-toggle-content.hide - if image.has_tags? diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 9167789a69d..6dffc026392 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -23,3 +23,7 @@ - disabled_title = @service.disabled_title = link_to 'Cancel', namespace_project_settings_integrations_path(@project.namespace, @project), class: 'btn btn-cancel' + +- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true) + %hr + = render "projects/services/#{@service.to_param}/show" diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml index 86d5a0ec7b8..997b702da33 100644 --- a/app/views/projects/services/_index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -1,9 +1,9 @@ .row.prepend-top-default.append-bottom-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0 Project services %p Project services allow you to integrate GitLab with other applications - .col-lg-9 + .col-lg-8 %table.table %colgroup %col diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml new file mode 100644 index 00000000000..c4ac384ca1a --- /dev/null +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -0,0 +1,45 @@ +- content_for :page_specific_javascripts do + = webpack_bundle_tag('prometheus_metrics') + +.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring + .col-lg-3 + %h4.prepend-top-0 + Metrics + %p + Metrics are automatically configured and monitored + based on a library of metrics from popular exporters. + = link_to 'More information', '#' + + .col-lg-9 + .panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{namespace_project_prometheus_active_metrics_path(@project.namespace, @project, :json)}" } } + .panel-heading + %h3.panel-title + Monitored + %span.badge.js-monitored-count 0 + .panel-body + .loading-metrics.text-center.js-loading-metrics + = icon('spinner spin 3x', class: 'metrics-load-spinner') + %p Finding and configuring metrics... + .empty-metrics.text-center.hidden.js-empty-metrics + = custom_icon('icon_empty_metrics') + %p No metrics are being monitored. To start monitoring, deploy to an environment. + = link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do + View environments + %ul.list-unstyled.metrics-list.hidden.js-metrics-list + + .panel.panel-default.hidden.js-panel-missing-env-vars + .panel-heading + %h3.panel-title + = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') + Missing environment variable + %span.badge.js-env-var-count 0 + .panel-body.hidden + .flash-container + .flash-notice + .flash-text + To set up automatic monitoring, add the environment variable + %code + $CI_ENVIRONMENT_SLUG + to exporter’s queries. + = link_to 'More information', '#' + %ul.list-unstyled.metrics-list.js-missing-var-metrics-list diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index e8d2e91bd76..00ccc3ec41e 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" unless fluid_layout - page_title "Pipelines" = render "projects/settings/head" diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index f69992566b5..1d1d0849289 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" unless fluid_layout - page_title 'Integrations' = render "projects/settings/head" = render 'projects/hooks/index' diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml index 343807b87cd..1e7695ac397 100644 --- a/app/views/projects/settings/members/show.html.haml +++ b/app/views/projects/settings/members/show.html.haml @@ -1,3 +1,5 @@ +- @content_class = "limit-container-width" unless fluid_layout + - page_title "Members" = render "projects/settings/head" diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 847f3c2f348..d8e448dd2af 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout - page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" = render 'shared/snippets/header' @@ -9,4 +10,4 @@ .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true - #notes= render "shared/notes/notes_with_form", :autocomplete => true + #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index de57cd4ba00..f9147815427 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,5 +1,5 @@ - if readme.rich_viewer - %article.file-holder.readme-holder + %article.file-holder.readme-holder{ class: ("limited-width-container" unless fluid_layout) } .js-file-title.file-title = blob_icon readme.mode, readme.name = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path)) do diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index abde2a48587..00da76349da 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,79 +1,81 @@ -.tree-controls - = render 'projects/find_file_link' - - = link_to s_('Commits|History'), namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-grouped' - - = render 'projects/buttons/download', project: @project, ref: @ref +.tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'tree', path: @path -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'tree', path: @path - -%ul.breadcrumb.repo-breadcrumb - %li - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - path_breadcrumbs do |title, path| + %ul.breadcrumb.repo-breadcrumb %li - = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + - path_breadcrumbs do |title, path| + %li + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path)) - - if current_user - %li - - if !on_top_of_branch? - %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } } - = icon('plus') - - else - %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown" } + - if current_user + %li + - if !on_top_of_branch? + %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } } = icon('plus') - %ul.dropdown-menu - - if can_edit_tree? - %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do - = icon('pencil fw') - #{ _('New file') } - %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do - = icon('file fw') - #{ _('Upload file') } - %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do - = icon('folder fw') - #{ _('New directory') } - - elsif can?(current_user, :fork_project, @project) - %li - - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, - continue: continue_params) - = link_to fork_path, method: :post do - = icon('pencil fw') - #{ _('New file') } + - else + %span.dropdown + %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown", "data-target" => ".add-to-tree-dropdown" } + = icon('plus') + .add-to-tree-dropdown + %ul.dropdown-menu + - if can_edit_tree? + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do + = icon('pencil fw') + #{ _('New file') } + %li + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do + = icon('file fw') + #{ _('Upload file') } + %li + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do + = icon('folder fw') + #{ _('New directory') } + - elsif can?(current_user, :fork_project, @project) + %li + - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('pencil fw') + #{ _('New file') } + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to upload a file again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('file fw') + #{ _('Upload file') } + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to create a new directory again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('folder fw') + #{ _('New directory') } + + %li.divider %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to upload a file again.", - notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, - continue: continue_params) - = link_to fork_path, method: :post do - = icon('file fw') - #{ _('Upload file') } + = link_to new_namespace_project_branch_path(@project.namespace, @project) do + = icon('code-fork fw') + #{ _('New branch') } %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to create a new directory again.", - notice_now: edit_in_new_fork_notice_now } - - fork_path = namespace_project_forks_path(@project.namespace, @project, namespace_key: current_user.namespace.id, - continue: continue_params) - = link_to fork_path, method: :post do - = icon('folder fw') - #{ _('New directory') } + = link_to new_namespace_project_tag_path(@project.namespace, @project) do + = icon('tags fw') + #{ _('New tag') } + +.tree-controls + = render 'projects/find_file_link' - %li.divider - %li - = link_to new_namespace_project_branch_path(@project.namespace, @project) do - = icon('code-fork fw') - #{ _('New branch') } - %li - = link_to new_namespace_project_tag_path(@project.namespace, @project) do - = icon('tags fw') - #{ _('New tag') } + = link_to s_('Commits|History'), namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn' + + = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index cc74e50a5e3..e9a2f803edd 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -1,7 +1,7 @@ .row.prepend-top-default.append-bottom-default.triggers-container - .col-lg-3 + .col-lg-4 = render "projects/triggers/content" - .col-lg-9 + .col-lg-8 .panel.panel-default .panel-heading %h4.panel-title diff --git a/app/views/projects/variables/_index.html.haml b/app/views/projects/variables/_index.html.haml index 1b852a9c5b3..5e6786f6698 100644 --- a/app/views/projects/variables/_index.html.haml +++ b/app/views/projects/variables/_index.html.haml @@ -1,7 +1,7 @@ .row.prepend-top-default.append-bottom-default - .col-lg-3 + .col-lg-4 = render "projects/variables/content" - .col-lg-9 + .col-lg-8 %h5.prepend-top-0 Add a variable = render "projects/variables/form", btn_text: "Add new variable" diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 4b98ff88241..2329de9e11f 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -2,7 +2,7 @@ - nonce = SecureRandom.hex - descriptions = local_assigns.slice(:message_with_description, :message_without_description) = label_tag "commit_message-#{nonce}", class: 'control-label' do - Commit message + #{ _('Commit message') } .col-sm-10 .commit-message-container .max-width-marker diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index c185e9b73ee..de0281e97c6 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,12 +1,13 @@ - label_css_id = dom_id(label) - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] +- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user %li{ id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown - %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } + %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } } Options = icon('caret-down') .dropdown-menu.dropdown-menu-align-right @@ -17,18 +18,18 @@ %li = link_to_label(label, subject: subject) do view open issues - - if current_user && defined?(@project) + - if current_user %li.label-subscription - - if label.is_a?(ProjectLabel) - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - %span= label_subscription_toggle_button_text(label, @project) - - else - %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } } + - if can_subscribe_to_label_in_different_levels?(label) + %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } %span Unsubscribe %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } %span Subscribe at project level %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } %span Subscribe at group level + - else + %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } } + %span= label_subscription_toggle_button_text(label, @project) - if can?(current_user, :admin_label, label) %li @@ -42,14 +43,10 @@ = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do view open issues - - if current_user && defined?(@project) + - if current_user .label-subscription.inline - - if label.is_a?(ProjectLabel) - %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - %span= label_subscription_toggle_button_text(label, @project) - = icon('spinner spin', class: 'label-subscribe-button-loading') - - else - %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } } + - if can_subscribe_to_label_in_different_levels?(label) + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } %span Unsubscribe = icon('spinner spin', class: 'label-subscribe-button-loading') @@ -59,10 +56,14 @@ = icon('chevron-down') %ul.dropdown-menu %li - %a.js-subscribe-button{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } Project level - %a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } } + %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } Group level + - else + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } } + %span= label_subscription_toggle_button_text(label, @project) + = icon('spinner spin', class: 'label-subscribe-button-loading') - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, 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 @@ -76,10 +77,10 @@ %span.sr-only Delete = icon('trash-o') - - if current_user && defined?(@project) - - if label.is_a?(ProjectLabel) + - if current_user + - if can_subscribe_to_label_in_different_levels?(label) :javascript - new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription'); + new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription'); - else :javascript - new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription'); + new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index d28f9421ecf..7b599dff0e3 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -4,9 +4,9 @@ = icon('bars') .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), dom_id: dom_id(label), type: label.type } } - %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } + %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' } = icon('star-o') - %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } + %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' } = icon('star') %span.label-name = link_to_label(label, subject: @project, tooltip: false) diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 25a56f84ec5..0a4a24ae807 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -5,16 +5,12 @@ - else - if can?(current_user, :push_code, @project) .form-group.branch - = label_tag 'branch_name', 'Target branch', class: 'control-label' + = label_tag 'branch_name', _('Target Branch'), class: 'control-label' .col-sm-10 = text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name" .js-create-merge-request-container - .checkbox - - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" - Start a <strong>new merge request</strong> with these changes + = render 'shared/new_merge_request_checkbox' - else = hidden_field_tag 'branch_name', @branch_name || tree_edit_branch = hidden_field_tag 'create_merge_request', 1 diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml new file mode 100644 index 00000000000..133c31f09c4 --- /dev/null +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -0,0 +1,8 @@ +.checkbox + - nonce = SecureRandom.hex + = label_tag "create_merge_request-#{nonce}" do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + - translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" } + - translation = _('Start a %{new_merge_request} with these changes') % translation_variables + #{ translation.html_safe } + diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml index b561e6dc248..9b1a467df6b 100644 --- a/app/views/shared/_no_password.html.haml +++ b/app/views/shared/_no_password.html.haml @@ -1,9 +1,8 @@ -- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? +- if show_no_password_message? .no-password-message.alert.alert-warning - - set_password_link = link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path - - translation_params = { protocol: gitlab_config.protocol.upcase, set_password_link: set_password_link } + - translation_params = { protocol: gitlab_config.protocol.upcase, set_password_link: link_to_set_password } - set_password_message = _("You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account") % translation_params - + = set_password_message.html_safe .alert-link-group = link_to _("Don't show again"), profile_path(user: {hide_no_password: true}), method: :put | diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index e7815e28017..17ef5327341 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,8 @@ -- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? +- if show_no_ssh_key_message? .no-ssh-key-message.alert.alert-warning - add_ssh_key_link = link_to s_('MissingSSHKeyWarningLink|add an SSH key'), profile_keys_path, class: 'alert-link' - ssh_message = _("You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile") % { add_ssh_key_link: add_ssh_key_link } - #{ ssh_message.html_safe } + = ssh_message.html_safe .alert-link-group = link_to _("Don't show again"), profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index 307d4919224..f65bb6a29e6 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -2,10 +2,10 @@ - model = local_assigns.fetch(:model) - form = local_assigns.fetch(:form) -- supports_slash_commands = model.new_record? +- supports_quick_actions = model.new_record? -- if supports_slash_commands - - preview_url = preview_markdown_path(project, slash_commands_target_type: model.class.name) +- if supports_quick_actions + - preview_url = preview_markdown_path(project, quick_actions_target_type: model.class.name) - else - preview_url = preview_markdown_path(project) @@ -17,7 +17,7 @@ = render 'projects/zen', f: form, attr: :description, classes: 'note-textarea', placeholder: "Write a comment or drag your files here...", - supports_slash_commands: supports_slash_commands - = render 'shared/notes/hints', supports_slash_commands: supports_slash_commands + supports_quick_actions: supports_quick_actions + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions .clearfix .error-alert diff --git a/app/views/shared/icons/_icon_empty_metrics.svg b/app/views/shared/icons/_icon_empty_metrics.svg new file mode 100644 index 00000000000..24fa353f3ba --- /dev/null +++ b/app/views/shared/icons/_icon_empty_metrics.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"> + <g fill="#E5E5E5"> + <path d="M32 64C30.8954305 64 30 63.1045695 30 62 30 60.8954305 30.8954305 60 32 60 33.8894444 60 35.7536611 59.8131396 37.574335 59.4454933 38.6570511 59.2268618 39.7120017 59.9273408 39.9306331 61.0100569 40.1492646 62.0927729 39.4487856 63.1477235 38.3660695 63.366355 36.285133 63.7865558 34.1557023 64 32 64zM49.2301062 58.9696428C51.0302775 57.8173242 52.7114504 56.4871355 54.247711 55.0008916 55.0415758 54.232873 55.0625283 52.9667164 54.2945097 52.1728516 53.5264912 51.3789869 52.2603346 51.3580344 51.4664698 52.1260529 50.1212672 53.4274592 48.6493395 54.5920875 47.0736141 55.6007347 46.1433158 56.1962335 45.8719072 57.4331365 46.4674061 58.3634348 47.0629049 59.2937331 48.2998079 59.5651416 49.2301062 58.9696428zM61.0426034 45.4531856C61.9412068 43.5163476 62.6441937 41.4911051 63.1388045 39.4034279 63.393449 38.3286117 62.7285685 37.2508708 61.6537523 36.9962262 60.5789361 36.7415816 59.5011952 37.4064621 59.2465506 38.4812784 58.8141946 40.3061875 58.1997219 42.0764286 57.4141077 43.7697311 56.9492346 44.7717126 57.3846469 45.9608331 58.3866284 46.4257062 59.3886098 46.8905793 60.5777303 46.455167 61.0426034 45.4531856zM63.7270657 27.8034151C63.4476841 25.6718707 62.9558906 23.5863203 62.2616468 21.5714028 61.9018246 20.527084 60.7635435 19.9721898 59.7192246 20.3320119 58.6749058 20.6918341 58.1200116 21.8301152 58.4798337 22.874434 59.0867105 24.6357842 59.5166381 26.45898 59.760988 28.3232492 59.9045362 29.4184513 60.9087418 30.1899192 62.0039439 30.046371 63.099146 29.9028228 63.8706139 28.8986173 63.7270657 27.8034151zM56.4699838 11.3781121C55.0919588 9.74451505 53.5537382 8.25140603 51.8798083 6.92273835 51.0146495 6.23602588 49.7566092 6.38068523 49.0698968 7.24584403 48.3831843 8.11100284 48.5278436 9.36904308 49.3930024 10.0557555 50.8587525 11.2191822 52.2058153 12.5267396 53.4125204 13.9572433 54.1247279 14.8015385 55.3865225 14.9086168 56.2308177 14.1964094 57.0751129 13.484202 57.1821912 12.2224073 56.4699838 11.3781121zM41.481294 1.42849704C39.4470333.798260231 37.3474846.371987025 35.2067823.158824109 34.1076485.0493765922 33.1278998.851675811 33.0184523 1.95080957 32.9090048 3.04994333 33.711304 4.02969203 34.8104377 4.13913955 36.6833634 4.32563829 38.5191483 4.69835932 40.297557 5.24933028 41.3526509 5.57621023 42.4729622 4.98587613 42.7998421 3.93078217 43.1267221 2.8756882 42.536388 1.75537699 41.481294 1.42849704zM23.6558195 1.0993008C21.5852929 1.6571259 19.5822296 2.42161363 17.6728876 3.37914679 16.6855233 3.874309 16.2865147 5.07613416 16.7816769 6.06349841 17.2768392 7.05086266 18.4786643 7.44987125 19.4660286 6.95470905 21.1354949 6.11747332 22.8864813 5.44919307 24.6963667 4.96158787 25.7629079 4.67424869 26.3945759 3.57671185 26.1072367 2.51017072 25.8198975 1.44362959 24.7223606.811961615 23.6558195 1.0993008zM8.36290105 10.4291871C6.92120358 12.00815 5.63985273 13.7275139 4.53998784 15.5610549 3.97179016 16.5082746 4.27904822 17.7367631 5.22626792 18.3049608 6.17348763 18.8731585 7.40197615 18.5659004 7.97017383 17.6186807 8.9327668 16.0139803 10.054503 14.5087932 11.3168098 13.126301 12.0615972 12.3106016 12.0041117 11.0455771 11.1884123 10.3007897 10.372713 9.55600224 9.10768848 9.61348772 8.36290105 10.4291871zM.450120287 26.6230259C.151304663 28.3883054 0 30.1850053 0 32 0 32.2974081.00406268322 32.594367.0121750297 32.8908218.0423897377 33.994978.96197903 34.8655796 2.0661352 34.8353649 3.17029137 34.8051502 4.04089294 33.8855609 4.01067824 32.7814047 4.00356366 32.521412 4 32.2609289 4 32 4 30.4089462 4.13249902 28.8355581 4.39401589 27.2906242 4.57836807 26.2015475 3.84494393 25.1692294 2.75586724 24.9848772 1.66679054 24.800525.634472466 25.5339492.450120287 26.6230259zM2.45830096 44.3202494C3.28286321 46.2952494 4.30407075 48.1806071 5.50459135 49.9494734 6.124886 50.8634254 7.36863868 51.1014818 8.28259072 50.4811871 9.19654276 49.8608925 9.43459912 48.6171398 8.81430448 47.7031878 7.76386025 46.1554464 6.87058107 44.5062706 6.14951581 42.7791677 5.72395784 41.7598668 4.55266835 41.2785432 3.53336751 41.7041011 2.51406668 42.1296591 2.03274299 43.3009486 2.45830096 44.3202494zM13.73374 58.2776222C15.4883094 59.4994144 17.3614388 60.5433005 19.3262717 61.39161 20.3403619 61.8294398 21.5173756 61.3622885 21.9552054 60.3481983 22.3930351 59.3341082 21.9258838 58.1570945 20.9117937 57.7192647 19.1934726 56.9773858 17.5548741 56.0642026 16.0195384 54.9950736 15.1130877 54.3638678 13.8665707 54.5869979 13.2353649 55.4934487 12.6041591 56.3998995 12.8272892 57.6464164 13.73374 58.2776222zM30.6955071 63.9738646C29.5918263 63.9295649 28.7330282 62.9989428 28.7773279 61.895262 28.8216276 60.7915812 29.7522497 59.9327832 30.8559305 59.9770829 31.2344492 59.9922759 31.6140624 59.9999282 31.9946308 59.9999995 33.0992003 60.0002065 33.994463 60.8958047 33.994256 62.0003742 33.9940491 63.1049437 33.0984508 64.0002064 31.9938814 63.9999994 31.5600677 63.9999181 31.1272192 63.9911927 30.6955071 63.9738646zM30.1721098 44.2840559C30.7941711 46.023825 33.2407935 46.0619159 33.9167124 44.3423547L38.9452693 31.5495297 41.1315797 35.2685507C41.4908522 35.8796908 42.1468005 36.2549751 42.8557214 36.2549751L51.1106965 36.2549751C52.215266 36.2549751 53.1106965 35.3595446 53.1106965 34.2549751 53.1106965 33.1504056 52.215266 32.2549751 51.1106965 32.2549751L43.9999712 32.2549751 40.3112064 25.9802055C39.465988 24.5424477 37.3358287 24.7099356 36.7257006 26.2621229L32.1439734 37.9181973 26.2115967 21.3266406C25.5807315 19.562249 23.0875908 19.5563214 22.4483429 21.3176933L18.4775633 32.2587065 13 32.2587065C11.8954305 32.2587065 11 33.154137 11 34.2587065 11 35.363276 11.8954305 36.2587065 13 36.2587065L19.8793532 36.2587065C20.720826 36.2587065 21.4722973 35.732004 21.7593685 34.9410132L24.314328 27.9011249 30.1721098 44.2840559z"/> + </g> +</svg> diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index a8a6d84128d..7cfdfb6e6ee 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -1,7 +1,7 @@ - type = local_assigns.fetch(:type) %aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" } - .issuable-sidebar + .issuable-sidebar.hidden = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do .block .filter-item.inline.update-issues-btn.pull-left diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index cf7ba52d840..3f03cc7a275 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,24 +1,25 @@ - type = local_assigns.fetch(:type, :issues) - page_context_word = type.to_s.humanize(capitalize: false) - issuables = @issues || @merge_requests -- closed_title = 'Filter by issues that are currently closed.' %ul.nav-links.issues-state-filters %li{ class: active_when(params[:state] == 'opened') }> - %button.btn.btn-link{ id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", type: 'button', data: { state: 'opened' } } + = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{ class: active_when(params[:state] == 'merged') }> - %button.btn.btn-link{ id: 'state-merged', title: 'Filter by merge requests that are currently merged.', type: 'button', data: { state: 'merged' } } + = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do #{issuables_state_counter_text(type, :merged)} - - closed_title = 'Filter by merge requests that are currently closed and unmerged.' - - %li{ class: active_when(params[:state] == 'closed') }> - %button.btn.btn-link{ id: 'state-closed', title: closed_title, type: 'button', data: { state: 'closed' } } - #{issuables_state_counter_text(type, :closed)} + %li{ class: active_when(params[:state] == 'closed') }> + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do + #{issuables_state_counter_text(type, :closed)} + - else + %li{ class: active_when(params[:state] == 'closed') }> + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do + #{issuables_state_counter_text(type, :closed)} %li{ class: active_when(params[:state] == 'all') }> - %button.btn.btn-link{ id: 'state-all', title: "Show all #{page_context_word}.", type: 'button', data: { state: 'all' } } + = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do #{issuables_state_counter_text(type, :all)} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index e49bd5ebb13..745f1ee62da 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('sidebar') -%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix", signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header @@ -20,7 +20,7 @@ .block.todo.hide-expanded = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true .block.assignee - = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable + = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? .block.milestone .sidebar-collapsed-icon = icon('clock-o', 'aria-hidden': 'true') diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index bcfa1dc826e..2ea5eb960c0 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -1,5 +1,5 @@ - if issuable.is_a?(Issue) - #js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]" } } + #js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]", signed_in: signed_in } } .title.hide-collapsed Assignee = icon('spinner spin') @@ -14,6 +14,9 @@ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' + - if !signed_in + %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } + = sidebar_gutter_toggle_icon .value.hide-collapsed - if issuable.assignee = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index 271150ed318..bfa91629e1e 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -3,7 +3,8 @@ - return unless issuable.is_a?(MergeRequest) - return if issuable.closed_without_fork? --# This check is duplicated below, to avoid conflicts with EE. +-# This check is duplicated below to avoid CE -> EE merge conflicts. +-# This comment and the following line should only exist in CE. - return unless issuable.can_remove_source_branch?(current_user) .form-group diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 22547a30cdf..a7c67ac9980 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -16,7 +16,7 @@ %strong #{project.name_with_namespace} · - if issuable.is_a?(Issue) = confidential_icon(issuable) - = link_to_gfm issuable.title, issuable_url_args, title: issuable.title + = link_to issuable.title, issuable_url_args, title: issuable.title .issuable-detail = link_to [project.namespace.becomes(Namespace), project, issuable] do %span.issuable-number= issuable.to_reference diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 8af3bd597c5..7175e275f95 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -8,11 +8,11 @@ = title - if show_counter .counter - = number_with_delimiter(issuables.size) + = number_with_delimiter(issuables.length) - class_prefix = dom_class(issuables).pluralize - %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } + %ul{ class: "well-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" } = render partial: 'shared/milestones/issuable', - collection: issuables.order_position_asc, + collection: issuables, as: :issuable, locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 9e6a76e1ddb..680e1f3a4ea 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -4,7 +4,7 @@ %li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } .row .col-sm-6 - %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path + %strong= link_to truncate(milestone.title, length: 100), milestone_path .col-sm-6 .pull-right.light #{milestone.percent_complete(current_user)}% complete .row diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 6a6d817b344..4de8a6cb15f 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -31,12 +31,12 @@ .tab-content.milestone-content - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name - .tab-pane#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } + = render 'shared/milestones/issues_tab', issues: milestone.sorted_issues(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-merge-requests -# loaded async = render "shared/milestones/tab_loading" - else - .tab-pane.active#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } } + .tab-pane.active#tab-merge-requests -# loaded async = render "shared/milestones/tab_loading" .tab-pane#tab-participants diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index eaf50bc2115..c6b5dcc3647 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -1,6 +1,7 @@ -- supports_slash_commands = note_supports_slash_commands?(@note) -- if supports_slash_commands - - preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id) +- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) +- supports_quick_actions = note_supports_quick_actions?(@note) +- if supports_quick_actions + - preview_url = preview_markdown_path(@project, quick_actions_target_type: @note.noteable_type, quick_actions_target_id: @note.noteable_id) - else - preview_url = preview_markdown_path(@project) @@ -27,8 +28,9 @@ attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here...", - supports_slash_commands: supports_slash_commands - = render 'shared/notes/hints', supports_slash_commands: supports_slash_commands + supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions .error-alert .note-form-actions.clearfix diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index 7ce6130de60..bc1ac3d8ac2 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -1,10 +1,10 @@ -- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) +- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) .comment-toolbar.clearfix .toolbar-text = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 - - if supports_slash_commands + - if supports_quick_actions and - = link_to 'slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 + = link_to 'quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1 are - else is diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 5902798dfd0..f0fcc414756 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -6,13 +6,14 @@ - if can_create_note? %ul.notes.notes-form.timeline %li.timeline-entry - .flash-container.timeline-content + .timeline-entry-inner + .flash-container.timeline-content - .timeline-icon.hidden-xs.hidden-sm - %a.author_link{ href: user_path(current_user) } - = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' - .timeline-content.timeline-content-form - = render "shared/notes/form", view: diff_view + .timeline-icon.hidden-xs.hidden-sm + %a.author_link{ href: user_path(current_user) } + = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' + .timeline-content.timeline-content-form + = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete - elsif !current_user .disabled-comment.text-center.prepend-top-default Please diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index 752932e6045..9186c2ba9c9 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -3,7 +3,7 @@ .modal-content .modal-header %button.close{ type: "button", "aria-label": "close", data: { dismiss: "modal" } } - %span{ "aria-hidden": "true" } } × + %span{ "aria-hidden": "true" } × %h4#custom-notifications-title.modal-title #{ _('Custom notification events') } diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index fbc335f6176..8c3d6351ac2 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -7,7 +7,7 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - cache_key = project_list_cache_key(project) -- updated_tooltip = time_ago_with_tooltip(project.last_activity_at) +- updated_tooltip = time_ago_with_tooltip(project.last_activity_date) %li.project-row{ class: css_class } = cache(cache_key) do diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 813d8d69d8d..17b34c5eeb3 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -16,7 +16,7 @@ - else = render "snippets/actions" -.snippet-header +.snippet-header.limited-header-width %h2.snippet-title.prepend-top-0.append-bottom-0 = markdown_field(@snippet, :title) diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 216184eb839..8818590362d 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout - page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" = render 'shared/snippets/header' @@ -9,4 +10,4 @@ .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true - #notes= render "shared/notes/notes_with_form", :autocomplete => false + #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index 79efca4f2f9..48e2da338f6 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -7,7 +7,7 @@ class MergeWorker current_user = User.find(current_user_id) merge_request = MergeRequest.find(merge_request_id) - MergeRequests::MergeService.new(merge_request.target_project, current_user, params). - execute(merge_request) + MergeRequests::MergeService.new(merge_request.target_project, current_user, params) + .execute(merge_request) end end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index fe6a49976e0..c0c03848a40 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -47,8 +47,8 @@ class ProcessCommitWorker # therefor we use IssueCollection here and skip the authorization check in # Issues::CloseService#execute. IssueCollection.new(issues).updatable_by_user(user).each do |issue| - Issues::CloseService.new(project, author). - close_issue(issue, commit: commit) + Issues::CloseService.new(project, author) + .close_issue(issue, commit: commit) end end @@ -57,8 +57,8 @@ class ProcessCommitWorker return if mentioned_issues.empty? - Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil). - update_all(first_mentioned_in_commit_at: commit.committed_date) + Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil) + .update_all(first_mentioned_in_commit_at: commit.committed_date) end def build_commit(project, hash) diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 8ff9d07860f..505ff9e086e 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -32,8 +32,8 @@ class ProjectCacheWorker private def try_obtain_lease_for(project_id, section) - Gitlab::ExclusiveLease. - new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT). - try_obtain + Gitlab::ExclusiveLease + .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT) + .try_obtain end end diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb index 5ce0e0405d0..6b607451c7a 100644 --- a/app/workers/propagate_service_template_worker.rb +++ b/app/workers/propagate_service_template_worker.rb @@ -14,8 +14,8 @@ class PropagateServiceTemplateWorker private def try_obtain_lease_for(template_id) - Gitlab::ExclusiveLease. - new("propagate_service_template_worker:#{template_id}", timeout: LEASE_TIMEOUT). - try_obtain + Gitlab::ExclusiveLease + .new("propagate_service_template_worker:#{template_id}", timeout: LEASE_TIMEOUT) + .try_obtain end end diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb index 392abb9c21b..2b43bb19ad1 100644 --- a/app/workers/prune_old_events_worker.rb +++ b/app/workers/prune_old_events_worker.rb @@ -10,9 +10,9 @@ class PruneOldEventsWorker '(id IN (SELECT id FROM (?) ids_to_remove))', Event.unscoped.where( 'created_at < ?', - (12.months + 1.day).ago). - select(:id). - limit(10_000)). - delete_all + (12.months + 1.day).ago) + .select(:id) + .limit(10_000)) + .delete_all end end diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index c3e7491ec4e..b94d83bd709 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -32,10 +32,10 @@ module RepositoryCheck # has to sit and wait for this query to finish. def project_ids limit = 10_000 - never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago). - limit(limit).pluck(:id) - old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago). - reorder('last_repository_check_at ASC').limit(limit).pluck(:id) + never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago) + .limit(limit).pluck(:id) + old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago) + .reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects end diff --git a/app/workers/update_user_activity_worker.rb b/app/workers/update_user_activity_worker.rb index b3c2f13aa33..31bbdb69edb 100644 --- a/app/workers/update_user_activity_worker.rb +++ b/app/workers/update_user_activity_worker.rb @@ -7,8 +7,8 @@ class UpdateUserActivityWorker ids = pairs.keys conditions = 'WHEN id = ? THEN ? ' * ids.length - User.where(id: ids). - update_all([ + User.where(id: ids) + .update_all([ "last_activity_on = CASE #{conditions} ELSE last_activity_on END", *pairs.to_a.flatten ]) diff --git a/bin/ci/upgrade.rb b/bin/ci/upgrade.rb deleted file mode 100755 index aab4f60ec60..00000000000 --- a/bin/ci/upgrade.rb +++ /dev/null @@ -1,3 +0,0 @@ -require_relative "../lib/ci/upgrader" - -Ci::Upgrader.new.execute diff --git a/changelogs/unreleased/10378-promote-blameless-culture.yml b/changelogs/unreleased/10378-promote-blameless-culture.yml deleted file mode 100644 index 8cf64dfd793..00000000000 --- a/changelogs/unreleased/10378-promote-blameless-culture.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Changed Blame to Annotate in the UI to promote blameless culture -merge_request: 10378 -author: Ilya Vassilevsky diff --git a/changelogs/unreleased/12151-add-since-and-until-params-to-issuables.yml b/changelogs/unreleased/12151-add-since-and-until-params-to-issuables.yml new file mode 100644 index 00000000000..2c915e62357 --- /dev/null +++ b/changelogs/unreleased/12151-add-since-and-until-params-to-issuables.yml @@ -0,0 +1,4 @@ +--- +title: Added "created_after" and "created_before" params to issuables +merge_request: 12151 +author: Kyle Bishop @kybishop diff --git a/changelogs/unreleased/12200-add-french-translation.yml b/changelogs/unreleased/12200-add-french-translation.yml new file mode 100644 index 00000000000..f31d982e0b9 --- /dev/null +++ b/changelogs/unreleased/12200-add-french-translation.yml @@ -0,0 +1,4 @@ +--- +title: "Adding French translations" +merge_request: 12200 +author : Erwan "Dremor" Georget diff --git a/changelogs/unreleased/12614-fix-long-message-from-mr.yml b/changelogs/unreleased/12614-fix-long-message-from-mr.yml deleted file mode 100644 index 30408ea4216..00000000000 --- a/changelogs/unreleased/12614-fix-long-message-from-mr.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Implement web hook logging -merge_request: 11027 -author: Alexander Randa diff --git a/changelogs/unreleased/12614-fix-long-message.yml b/changelogs/unreleased/12614-fix-long-message.yml deleted file mode 100644 index 94f8127c3c1..00000000000 --- a/changelogs/unreleased/12614-fix-long-message.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix long urls in the title of commit -merge_request: 10938 -author: Alexander Randa diff --git a/changelogs/unreleased/12910-snippets-description.yml b/changelogs/unreleased/12910-snippets-description.yml deleted file mode 100644 index ac3d754fee1..00000000000 --- a/changelogs/unreleased/12910-snippets-description.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support descriptions for snippets -merge_request: -author: diff --git a/changelogs/unreleased/13336-multiple-broadcast-messages.yml b/changelogs/unreleased/13336-multiple-broadcast-messages.yml new file mode 100644 index 00000000000..7dc73e1c6ea --- /dev/null +++ b/changelogs/unreleased/13336-multiple-broadcast-messages.yml @@ -0,0 +1,4 @@ +--- +title: Display all current broadcast messages, not just the last one +merge_request: 11113 +author: rickettm diff --git a/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml b/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml deleted file mode 100644 index 9c17c3b949c..00000000000 --- a/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce an Events API -merge_request: 11755 -author: diff --git a/changelogs/unreleased/17489-hide-code-from-guests.yml b/changelogs/unreleased/17489-hide-code-from-guests.yml deleted file mode 100644 index eb6daffedfe..00000000000 --- a/changelogs/unreleased/17489-hide-code-from-guests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Hide clone panel and file list when user is only a guest -merge_request: -author: James Clark diff --git a/changelogs/unreleased/18927-reorder-issue-action-buttons.yml b/changelogs/unreleased/18927-reorder-issue-action-buttons.yml deleted file mode 100644 index 793d6582940..00000000000 --- a/changelogs/unreleased/18927-reorder-issue-action-buttons.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reorder Issue action buttons in order of usability -merge_request: 11642 -author: diff --git a/changelogs/unreleased/19107-404-when-creating-new-milestone-or-issue-for-project-that-has-issues-disabled.yml b/changelogs/unreleased/19107-404-when-creating-new-milestone-or-issue-for-project-that-has-issues-disabled.yml deleted file mode 100644 index bec9aa34761..00000000000 --- a/changelogs/unreleased/19107-404-when-creating-new-milestone-or-issue-for-project-that-has-issues-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'New issue'/'New merge request' dropdowns should show only projects with issues/merge requests feature enabled -merge_request: 19107 -author: blackst0ne diff --git a/changelogs/unreleased/20517-delete-projects-issuescontroller-redirect_old.yml b/changelogs/unreleased/20517-delete-projects-issuescontroller-redirect_old.yml deleted file mode 100644 index 1f3ab3a2c10..00000000000 --- a/changelogs/unreleased/20517-delete-projects-issuescontroller-redirect_old.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove redirect for old issue url containing id instead of iid -merge_request: 11135 -author: blackst0ne diff --git a/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml deleted file mode 100644 index b350b27d863..00000000000 --- a/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace 'starred_projects.feature' spinach test with an rspec analog -merge_request: 11752 -author: blackst0ne diff --git a/changelogs/unreleased/23036-replace-dashboard-mr-spinach.yml b/changelogs/unreleased/23036-replace-dashboard-mr-spinach.yml new file mode 100644 index 00000000000..07c201de96e --- /dev/null +++ b/changelogs/unreleased/23036-replace-dashboard-mr-spinach.yml @@ -0,0 +1,4 @@ +--- +title: Replace 'dashboard/merge_requests' spinach with rspec +merge_request: 12440 +author: Alexander Randa (@randaalex) diff --git a/changelogs/unreleased/23036-replace-dashboard-todo-spinach.yml b/changelogs/unreleased/23036-replace-dashboard-todo-spinach.yml new file mode 100644 index 00000000000..65df9a836a5 --- /dev/null +++ b/changelogs/unreleased/23036-replace-dashboard-todo-spinach.yml @@ -0,0 +1,4 @@ +--- +title: Replace 'dashboard/todos' spinach with rspec +merge_request: 12453 +author: Alexander Randa (@randaalex) diff --git a/changelogs/unreleased/23603-add-extra-functionality-for-the-top-right-button.yml b/changelogs/unreleased/23603-add-extra-functionality-for-the-top-right-button.yml deleted file mode 100644 index 77f8e31e16e..00000000000 --- a/changelogs/unreleased/23603-add-extra-functionality-for-the-top-right-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add extra context-sensitive functionality for the top right menu button -merge_request: 11632 -author: diff --git a/changelogs/unreleased/23998-blame-age-map.yml b/changelogs/unreleased/23998-blame-age-map.yml new file mode 100644 index 00000000000..26a38f0939c --- /dev/null +++ b/changelogs/unreleased/23998-blame-age-map.yml @@ -0,0 +1,4 @@ +--- +title: Add blame view age mapping +merge_request: 7198 +author: Jeff Stubler diff --git a/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml b/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml deleted file mode 100644 index dbd8a538d51..00000000000 --- a/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Automatically adjust project settings to match changes in project visibility -merge_request: 11831 -author: diff --git a/changelogs/unreleased/24196-protected-variables.yml b/changelogs/unreleased/24196-protected-variables.yml deleted file mode 100644 index 71567a9d794..00000000000 --- a/changelogs/unreleased/24196-protected-variables.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add protected variables which would only be passed to protected branches or - protected tags -merge_request: 11688 -author: diff --git a/changelogs/unreleased/24373-warning-message-go-away.yml b/changelogs/unreleased/24373-warning-message-go-away.yml deleted file mode 100644 index c0f2fd260ba..00000000000 --- a/changelogs/unreleased/24373-warning-message-go-away.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Notes: Warning message should go away once resolved' -merge_request: 10823 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/25102-files-view-button.yml b/changelogs/unreleased/25102-files-view-button.yml new file mode 100644 index 00000000000..4ba815d9464 --- /dev/null +++ b/changelogs/unreleased/25102-files-view-button.yml @@ -0,0 +1,4 @@ +--- +title: Fix mobile view of files view buttons +merge_request: +author: diff --git a/changelogs/unreleased/25164-disable-fork-on-project-limit.yml b/changelogs/unreleased/25164-disable-fork-on-project-limit.yml new file mode 100644 index 00000000000..9fa824b161d --- /dev/null +++ b/changelogs/unreleased/25164-disable-fork-on-project-limit.yml @@ -0,0 +1,4 @@ +--- +title: Disable fork button on project limit +merge_request: 12145 +author: Ivan Chernov diff --git a/changelogs/unreleased/25373-jira-links.yml b/changelogs/unreleased/25373-jira-links.yml deleted file mode 100644 index 09589d4b992..00000000000 --- a/changelogs/unreleased/25373-jira-links.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don’t create comment on JIRA if it already exists for the entity -merge_request: -author: diff --git a/changelogs/unreleased/25426-group-dashboard-ui.yml b/changelogs/unreleased/25426-group-dashboard-ui.yml deleted file mode 100644 index cc2bf62d07b..00000000000 --- a/changelogs/unreleased/25426-group-dashboard-ui.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update Dashboard Groups UI with better support for subgroups -merge_request: -author: diff --git a/changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml b/changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml deleted file mode 100644 index af9fe3b5041..00000000000 --- a/changelogs/unreleased/25680-CI_ENVIRONMENT_URL.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add $CI_ENVIRONMENT_URL to predefined variables for pipelines -merge_request: 11695 -author: diff --git a/changelogs/unreleased/26212-upload-user-avatar-trough-api.yml b/changelogs/unreleased/26212-upload-user-avatar-trough-api.yml new file mode 100644 index 00000000000..667454ae95d --- /dev/null +++ b/changelogs/unreleased/26212-upload-user-avatar-trough-api.yml @@ -0,0 +1,4 @@ +--- +title: Accept image for avatar in user API +merge_request: 12143 +author: Ivan Chernov diff --git a/changelogs/unreleased/26325-system-hooks.yml b/changelogs/unreleased/26325-system-hooks.yml deleted file mode 100644 index 62b8adaeccd..00000000000 --- a/changelogs/unreleased/26325-system-hooks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Backported new SystemHook event: `repository_update`' -merge_request: 11140 -author: diff --git a/changelogs/unreleased/27070-rename-slash-commands-to-quick-actions.yml b/changelogs/unreleased/27070-rename-slash-commands-to-quick-actions.yml new file mode 100644 index 00000000000..497239db808 --- /dev/null +++ b/changelogs/unreleased/27070-rename-slash-commands-to-quick-actions.yml @@ -0,0 +1,5 @@ +--- +title: Rename "Slash commands" to "Quick actions" and deprecate "chat commands" in favor + of "slash commands" +merge_request: +author: diff --git a/changelogs/unreleased/27148-limit-bulk-create-memberships.yml b/changelogs/unreleased/27148-limit-bulk-create-memberships.yml deleted file mode 100644 index ac4aba2f4e0..00000000000 --- a/changelogs/unreleased/27148-limit-bulk-create-memberships.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Limit non-administrators to adding 100 members at a time to groups and projects -merge_request: 11940 -author: diff --git a/changelogs/unreleased/27439-memory-usage-info.yml b/changelogs/unreleased/27439-memory-usage-info.yml deleted file mode 100644 index dd212853f57..00000000000 --- a/changelogs/unreleased/27439-memory-usage-info.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add performance deltas between app deployments on Merge Request widget -merge_request: 11730 -author: diff --git a/changelogs/unreleased/27614-improve-instant-comments-exp.yml b/changelogs/unreleased/27614-improve-instant-comments-exp.yml deleted file mode 100644 index 4db676801f1..00000000000 --- a/changelogs/unreleased/27614-improve-instant-comments-exp.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve user experience around slash commands in instant comments -merge_request: 11612 -author: diff --git a/changelogs/unreleased/27645-html-email-brackets-bug.yml b/changelogs/unreleased/27645-html-email-brackets-bug.yml new file mode 100644 index 00000000000..e8004d03884 --- /dev/null +++ b/changelogs/unreleased/27645-html-email-brackets-bug.yml @@ -0,0 +1,4 @@ +--- +title: Fix an email parsing bug where brackets would be inserted in emails from some Outlook clients +merge_request: 9045 +author: jneen diff --git a/changelogs/unreleased/27697-make-arrow-icons-consistent-in-dropdown.yml b/changelogs/unreleased/27697-make-arrow-icons-consistent-in-dropdown.yml new file mode 100644 index 00000000000..92b5b59f46f --- /dev/null +++ b/changelogs/unreleased/27697-make-arrow-icons-consistent-in-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Use fa-chevron-down on dropdown arrows for consistency +merge_request: 9659 +author: TM Lee diff --git a/changelogs/unreleased/28080-system-checks.yml b/changelogs/unreleased/28080-system-checks.yml deleted file mode 100644 index 7d83014279a..00000000000 --- a/changelogs/unreleased/28080-system-checks.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactored gitlab:app:check into SystemCheck liberary and improve some checks -merge_request: 9173 -author: diff --git a/changelogs/unreleased/28139-use-color-input-broadcast-messages.yml b/changelogs/unreleased/28139-use-color-input-broadcast-messages.yml new file mode 100644 index 00000000000..97ebabaff1c --- /dev/null +++ b/changelogs/unreleased/28139-use-color-input-broadcast-messages.yml @@ -0,0 +1,4 @@ +--- +title: Use color inputs for broadcast messages +merge_request: +author: diff --git a/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml b/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml deleted file mode 100644 index 9cf8d745f92..00000000000 --- a/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Confirm Project forking behaviour via the API -merge_request: -author: diff --git a/changelogs/unreleased/28694-hard-delete-user-from-admin-panel.yml b/changelogs/unreleased/28694-hard-delete-user-from-admin-panel.yml deleted file mode 100644 index 2308a528580..00000000000 --- a/changelogs/unreleased/28694-hard-delete-user-from-admin-panel.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow users to be hard-deleted from the admin panel -merge_request: 11874 -author: diff --git a/changelogs/unreleased/28694-hard-delete-user-from-api.yml b/changelogs/unreleased/28694-hard-delete-user-from-api.yml deleted file mode 100644 index ad46540495c..00000000000 --- a/changelogs/unreleased/28694-hard-delete-user-from-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow users to be hard-deleted from the API -merge_request: 11853 -author: diff --git a/changelogs/unreleased/28717-support-additional-prometheus-metrics.yml b/changelogs/unreleased/28717-support-additional-prometheus-metrics.yml new file mode 100644 index 00000000000..720a79b8e1c --- /dev/null +++ b/changelogs/unreleased/28717-support-additional-prometheus-metrics.yml @@ -0,0 +1,4 @@ +--- +title: Additional Prometheus metrics support +merge_request: 11712 +author: diff --git a/changelogs/unreleased/29010-perf-bar.yml b/changelogs/unreleased/29010-perf-bar.yml deleted file mode 100644 index f4167e5562f..00000000000 --- a/changelogs/unreleased/29010-perf-bar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add an optional performance bar to view performance metrics for the current page -merge_request: 11439 -author: diff --git a/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml b/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml deleted file mode 100644 index 99c55f128e3..00000000000 --- a/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add prometheus based metrics collection to gitlab webapp -merge_request: -author: diff --git a/changelogs/unreleased/29690-rotate-otp-key-base.yml b/changelogs/unreleased/29690-rotate-otp-key-base.yml deleted file mode 100644 index 94d73a24758..00000000000 --- a/changelogs/unreleased/29690-rotate-otp-key-base.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add a Rake task to aid in rotating otp_key_base -merge_request: 11881 -author: diff --git a/changelogs/unreleased/29852-latex-formatting.yml b/changelogs/unreleased/29852-latex-formatting.yml deleted file mode 100644 index e96cda1d6b3..00000000000 --- a/changelogs/unreleased/29852-latex-formatting.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix LaTeX formatting for AsciiDoc wiki -merge_request: 11212 -author: diff --git a/changelogs/unreleased/30213-project-transfer-move-rollback.yml b/changelogs/unreleased/30213-project-transfer-move-rollback.yml new file mode 100644 index 00000000000..3eb1e399c54 --- /dev/null +++ b/changelogs/unreleased/30213-project-transfer-move-rollback.yml @@ -0,0 +1,4 @@ +--- +title: Rollback project repo move if there is an error in Projects::TransferService +merge_request: 11877 +author: diff --git a/changelogs/unreleased/30378-simplified-repository-settings-page.yml b/changelogs/unreleased/30378-simplified-repository-settings-page.yml deleted file mode 100644 index e8b87c8bb33..00000000000 --- a/changelogs/unreleased/30378-simplified-repository-settings-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Simplify project repository settings page -merge_request: 11698 -author: diff --git a/changelogs/unreleased/30410-revert-9347-and-10079.yml b/changelogs/unreleased/30410-revert-9347-and-10079.yml deleted file mode 100644 index 0149209caf2..00000000000 --- a/changelogs/unreleased/30410-revert-9347-and-10079.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Revert the feature that would include the current user's username in the HTTP - clone URL -merge_request: 11792 -author: diff --git a/changelogs/unreleased/30469-convdev-index.yml b/changelogs/unreleased/30469-convdev-index.yml deleted file mode 100644 index 0bdd9c4a699..00000000000 --- a/changelogs/unreleased/30469-convdev-index.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add ConvDev Index page to admin area -merge_request: 11377 -author: diff --git a/changelogs/unreleased/30651-improve-container-registry-description.yml b/changelogs/unreleased/30651-improve-container-registry-description.yml deleted file mode 100644 index 0157c9885bc..00000000000 --- a/changelogs/unreleased/30651-improve-container-registry-description.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add changelog for improved Registry description -merge_request: 11816 -author: diff --git a/changelogs/unreleased/30725-reset-user-limits-when-unchecking-external-user.yml b/changelogs/unreleased/30725-reset-user-limits-when-unchecking-external-user.yml new file mode 100644 index 00000000000..3058404b3f8 --- /dev/null +++ b/changelogs/unreleased/30725-reset-user-limits-when-unchecking-external-user.yml @@ -0,0 +1,4 @@ +--- +title: Ensures default user limits when external user is unchecked +merge_request: 12218 +author: diff --git a/changelogs/unreleased/30827-changes-to-audit-log.yml b/changelogs/unreleased/30827-changes-to-audit-log.yml deleted file mode 100644 index 32db3bf8e95..00000000000 --- a/changelogs/unreleased/30827-changes-to-audit-log.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Renamed users 'Audit Log'' to 'Authentication Log' -merge_request: 11400 -author: diff --git a/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml b/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml deleted file mode 100644 index 26ce84697d0..00000000000 --- a/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add API support for pipeline schedule -merge_request: 11307 -author: dosuken123 diff --git a/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml b/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml deleted file mode 100644 index c9bd2dc465e..00000000000 --- a/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Fix: Wiki is not searchable with Guest permissions' -merge_request: -author: diff --git a/changelogs/unreleased/30949-empty-states.yml b/changelogs/unreleased/30949-empty-states.yml deleted file mode 100644 index bef87a954b7..00000000000 --- a/changelogs/unreleased/30949-empty-states.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Center all empty states -merge_request: -author: diff --git a/changelogs/unreleased/31061-26135-ci-project-slug-enviroment-variables.yml b/changelogs/unreleased/31061-26135-ci-project-slug-enviroment-variables.yml deleted file mode 100644 index e71910dbd67..00000000000 --- a/changelogs/unreleased/31061-26135-ci-project-slug-enviroment-variables.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add slugify project path to CI enviroment variables -merge_request: 11838 -author: Ivan Chernov diff --git a/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml b/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml deleted file mode 100644 index 8d586616e07..00000000000 --- a/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove 'New issue' button when issues search returns no results. -merge_request: !11263 -author: diff --git a/changelogs/unreleased/31415-responsive-pipelines-table-2.yml b/changelogs/unreleased/31415-responsive-pipelines-table-2.yml new file mode 100644 index 00000000000..59402b85871 --- /dev/null +++ b/changelogs/unreleased/31415-responsive-pipelines-table-2.yml @@ -0,0 +1,4 @@ +--- +title: Create responsive mobile view for pipelines table +merge_request: +author: diff --git a/changelogs/unreleased/31448-jira-urls.yml b/changelogs/unreleased/31448-jira-urls.yml deleted file mode 100644 index d0e39f61b55..00000000000 --- a/changelogs/unreleased/31448-jira-urls.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add API URL to JIRA settings -merge_request: -author: diff --git a/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml b/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml deleted file mode 100644 index 88e79e3b6ea..00000000000 --- a/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disallow multiple selections for Milestone dropdown -merge_request: 11084 -author: diff --git a/changelogs/unreleased/31483-ordered-task-list.yml b/changelogs/unreleased/31483-ordered-task-list.yml deleted file mode 100644 index c43915b3268..00000000000 --- a/changelogs/unreleased/31483-ordered-task-list.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Ordered Task List Items -merge_request: 31483 -author: Jared Deckard <jared.deckard@gmail.com> diff --git a/changelogs/unreleased/31510-mask-password-field-edit.yml b/changelogs/unreleased/31510-mask-password-field-edit.yml deleted file mode 100644 index 0ef37be328d..00000000000 --- a/changelogs/unreleased/31510-mask-password-field-edit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update password field label while editing service settings -merge_request: 11431 -author: diff --git a/changelogs/unreleased/31511-jira-settings.yml b/changelogs/unreleased/31511-jira-settings.yml deleted file mode 100644 index 4f9ddb13ef6..00000000000 --- a/changelogs/unreleased/31511-jira-settings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Simplify testing and saving service integrations -merge_request: 11599 -author: diff --git a/changelogs/unreleased/31554-update-rufus-scheduler-and-sidekiq.yml b/changelogs/unreleased/31554-update-rufus-scheduler-and-sidekiq.yml deleted file mode 100644 index 0a36b52d561..00000000000 --- a/changelogs/unreleased/31554-update-rufus-scheduler-and-sidekiq.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update gem sidekiq-cron from 0.4.4 to 0.6.0 and rufus-scheduler from 3.1.10 - to 3.4.0 -merge_request: 10976 -author: dosuken123 diff --git a/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml deleted file mode 100644 index 4137050a077..00000000000 --- a/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix the last coverage in trace log should be extracted -merge_request: 11128 -author: dosuken123 diff --git a/changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml deleted file mode 100644 index 00957f7e4f7..00000000000 --- a/changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display Shared Runner status in Admin Dashboard -merge_request: 11783 -author: Ivan Chernov diff --git a/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml b/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml deleted file mode 100644 index 6dc48d6b2d8..00000000000 --- a/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add server uptime to System Info page in admin dashboard -merge_request: 11590 -author: Justin Boltz diff --git a/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml b/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml deleted file mode 100644 index aae760b0ef5..00000000000 --- a/changelogs/unreleased/31625-tag-editor-loses-all-inputs-when-you-try-to-add-a-tag-that-already-exists.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Keep input data after creating a tag that already exists -merge_request: 11155 -author: diff --git a/changelogs/unreleased/31633-animate-issue.yml b/changelogs/unreleased/31633-animate-issue.yml deleted file mode 100644 index 6df4135b09c..00000000000 --- a/changelogs/unreleased/31633-animate-issue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: animate adding issue to boards -merge_request: -author: diff --git a/changelogs/unreleased/31644-make-cookie-sessions-unique.yml b/changelogs/unreleased/31644-make-cookie-sessions-unique.yml deleted file mode 100644 index e9a6a32cf70..00000000000 --- a/changelogs/unreleased/31644-make-cookie-sessions-unique.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update session cookie key name to be unique to instance in development -merge_request: -author: diff --git a/changelogs/unreleased/31757-single-click-on-filter-in-search-bar-to-activate-dropdown.yml b/changelogs/unreleased/31757-single-click-on-filter-in-search-bar-to-activate-dropdown.yml deleted file mode 100644 index 48b8a8507ec..00000000000 --- a/changelogs/unreleased/31757-single-click-on-filter-in-search-bar-to-activate-dropdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Single click on filter to open filtered search dropdown -merge_request: -author: diff --git a/changelogs/unreleased/31781-print-rendered-files-not-possible.yml b/changelogs/unreleased/31781-print-rendered-files-not-possible.yml deleted file mode 100644 index 14915823ff7..00000000000 --- a/changelogs/unreleased/31781-print-rendered-files-not-possible.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Include the blob content when printing a blob page -merge_request: 11247 -author: diff --git a/changelogs/unreleased/31840-add-a-rubocop-that-forbids-redirect_to-inside-a-controller-destroy-action-without-an-explicit-status.yml b/changelogs/unreleased/31840-add-a-rubocop-that-forbids-redirect_to-inside-a-controller-destroy-action-without-an-explicit-status.yml deleted file mode 100644 index 52bfe771e2b..00000000000 --- a/changelogs/unreleased/31840-add-a-rubocop-that-forbids-redirect_to-inside-a-controller-destroy-action-without-an-explicit-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add a rubocop rule to check if a method 'redirect_to' is used without explicitly set 'status' in 'destroy' actions of controllers -merge_request: 11749 -author: @blackst0ne diff --git a/changelogs/unreleased/31849-pipeline-real-time-header.yml b/changelogs/unreleased/31849-pipeline-real-time-header.yml deleted file mode 100644 index 2bb7af897ff..00000000000 --- a/changelogs/unreleased/31849-pipeline-real-time-header.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Makes header information of pipeline show page realtine -merge_request: -author: diff --git a/changelogs/unreleased/31849-pipeline-show-view-realtime.yml b/changelogs/unreleased/31849-pipeline-show-view-realtime.yml deleted file mode 100644 index 838a769a26e..00000000000 --- a/changelogs/unreleased/31849-pipeline-show-view-realtime.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Creates a mediator for pipeline details vue in order to mount several vue apps - with the same data -merge_request: -author: diff --git a/changelogs/unreleased/31902-namespace-recent-searches-to-project.yml b/changelogs/unreleased/31902-namespace-recent-searches-to-project.yml deleted file mode 100644 index e00eb6d8f72..00000000000 --- a/changelogs/unreleased/31902-namespace-recent-searches-to-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Scope issue/merge request recent searches to project -merge_request: -author: diff --git a/changelogs/unreleased/3191-deploy-keys-update.yml b/changelogs/unreleased/3191-deploy-keys-update.yml deleted file mode 100644 index 4100163e94f..00000000000 --- a/changelogs/unreleased/3191-deploy-keys-update.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Implement ability to update deploy keys -merge_request: 10383 -author: Alexander Randa diff --git a/changelogs/unreleased/31943-document-go-183.yml b/changelogs/unreleased/31943-document-go-183.yml deleted file mode 100644 index 201cd48f1ab..00000000000 --- a/changelogs/unreleased/31943-document-go-183.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Upgrade dependency to Go 1.8.3 -merge_request: 31943 diff --git a/changelogs/unreleased/31983-increase-merge-request-diff-file-size-limit-for-default-toggle-opening.yml b/changelogs/unreleased/31983-increase-merge-request-diff-file-size-limit-for-default-toggle-opening.yml deleted file mode 100644 index f61aa0a6b6e..00000000000 --- a/changelogs/unreleased/31983-increase-merge-request-diff-file-size-limit-for-default-toggle-opening.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Increase individual diff collapse limit to 100 KB, and render limit to 200 KB -merge_request: -author: diff --git a/changelogs/unreleased/31998-pipelines-empty-state.yml b/changelogs/unreleased/31998-pipelines-empty-state.yml deleted file mode 100644 index 78ae222255e..00000000000 --- a/changelogs/unreleased/31998-pipelines-empty-state.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Pipelines table empty state - only render empty state if we receive 0 pipelines -merge_request: -author: diff --git a/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml b/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml deleted file mode 100644 index 0fd248e0400..00000000000 --- a/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable reference prefixes in notes for Snippets -merge_request: 11278 -author: diff --git a/changelogs/unreleased/32118-new-environment-btn-copy.yml b/changelogs/unreleased/32118-new-environment-btn-copy.yml deleted file mode 100644 index 16a51c3db6a..00000000000 --- a/changelogs/unreleased/32118-new-environment-btn-copy.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make New environment empty state btn lowercase -merge_request: -author: diff --git a/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml b/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml deleted file mode 100644 index 7fb3cb3a30b..00000000000 --- a/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Cache npm modules between pipelines with yarn to speed up setup-test-env -merge_request: 11343 -author: diff --git a/changelogs/unreleased/32301-filter-archive-project-on-param-present.yml b/changelogs/unreleased/32301-filter-archive-project-on-param-present.yml new file mode 100644 index 00000000000..d6534ed4e1a --- /dev/null +++ b/changelogs/unreleased/32301-filter-archive-project-on-param-present.yml @@ -0,0 +1,4 @@ +--- +title: Filter archived project in API v3 only if param present +merge_request: 12245 +author: Ivan Chernov diff --git a/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml b/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml deleted file mode 100644 index d2be3d6cc4b..00000000000 --- a/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Removes duplicate environment variable in documentation -merge_request: -author: diff --git a/changelogs/unreleased/32418-make-link-to-self-less-obvious.yml b/changelogs/unreleased/32418-make-link-to-self-less-obvious.yml deleted file mode 100644 index aabe87dac0f..00000000000 --- a/changelogs/unreleased/32418-make-link-to-self-less-obvious.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change links in issuable meta to black -merge_request: -author: diff --git a/changelogs/unreleased/32470-pag-links.yml b/changelogs/unreleased/32470-pag-links.yml new file mode 100644 index 00000000000..d0fd284f3ee --- /dev/null +++ b/changelogs/unreleased/32470-pag-links.yml @@ -0,0 +1,4 @@ +--- +title: more visual contrast in pagination widget +merge_request: +author: diff --git a/changelogs/unreleased/32570-project-activity-tab-border.yml b/changelogs/unreleased/32570-project-activity-tab-border.yml deleted file mode 100644 index 100a3e6a74d..00000000000 --- a/changelogs/unreleased/32570-project-activity-tab-border.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix border-bottom for project activity tab -merge_request: -author: diff --git a/changelogs/unreleased/32598-avoid-resource-intensive-login-checks-if-password-is-not-provided-for-git-http.yml b/changelogs/unreleased/32598-avoid-resource-intensive-login-checks-if-password-is-not-provided-for-git-http.yml deleted file mode 100644 index 6da7491bbda..00000000000 --- a/changelogs/unreleased/32598-avoid-resource-intensive-login-checks-if-password-is-not-provided-for-git-http.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Avoid resource intensive login checks if password is not provided. -merge_request: 11537 -author: Horatiu Eugen Vlad diff --git a/changelogs/unreleased/32642_last_commit_id_in_file_api.yml b/changelogs/unreleased/32642_last_commit_id_in_file_api.yml deleted file mode 100644 index 80435352e10..00000000000 --- a/changelogs/unreleased/32642_last_commit_id_in_file_api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Introduce optimistic locking support via optional parameter last_commit_sha on File Update API' -merge_request: 11694 -author: electroma diff --git a/changelogs/unreleased/32682-skipped-ci-icon.yml b/changelogs/unreleased/32682-skipped-ci-icon.yml deleted file mode 100644 index ad498b51900..00000000000 --- a/changelogs/unreleased/32682-skipped-ci-icon.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds new icon for CI skipped status -merge_request: -author: diff --git a/changelogs/unreleased/32720-emoji-spacing.yml b/changelogs/unreleased/32720-emoji-spacing.yml deleted file mode 100644 index da3df0f9093..00000000000 --- a/changelogs/unreleased/32720-emoji-spacing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Create equal padding for emoji -merge_request: -author: diff --git a/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml deleted file mode 100644 index a58f3a7429e..00000000000 --- a/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline_schedules pages throwing error 500 -merge_request: 11706 -author: dosuken123 diff --git a/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml b/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml deleted file mode 100644 index 9c1c1fe77f2..00000000000 --- a/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove redundant data-turbolink attributes from links -merge_request: 11672 -author: blackst0ne diff --git a/changelogs/unreleased/32807-company-icon.yml b/changelogs/unreleased/32807-company-icon.yml deleted file mode 100644 index 718108d3733..00000000000 --- a/changelogs/unreleased/32807-company-icon.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use briefcase icon for company in profile page -merge_request: -author: diff --git a/changelogs/unreleased/32832-confidential-issue-overflow.yml b/changelogs/unreleased/32832-confidential-issue-overflow.yml deleted file mode 100644 index 7d3d3bfed2e..00000000000 --- a/changelogs/unreleased/32832-confidential-issue-overflow.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove overflow from comment form for confidential issues and vertically aligns - confidential issue icon -merge_request: -author: diff --git a/changelogs/unreleased/32851-postgres-min-version.yml b/changelogs/unreleased/32851-postgres-min-version.yml deleted file mode 100644 index 139307d65c6..00000000000 --- a/changelogs/unreleased/32851-postgres-min-version.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Minimum postgresql version is now 9.2 -merge_request: 11677 -author: diff --git a/changelogs/unreleased/32955-special-keywords.yml b/changelogs/unreleased/32955-special-keywords.yml deleted file mode 100644 index 0f9939ced8c..00000000000 --- a/changelogs/unreleased/32955-special-keywords.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add all pipeline sources as special keywords to 'only' and 'except' -merge_request: 11844 -author: Filip Krakowski diff --git a/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml b/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml deleted file mode 100644 index eca42176501..00000000000 --- a/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Keep trailing newline when resolving conflicts by picking sides -merge_request: -author: diff --git a/changelogs/unreleased/32992-consider-using-zopfli-over-standard-gzip-compression-for-webpack-assets.yml b/changelogs/unreleased/32992-consider-using-zopfli-over-standard-gzip-compression-for-webpack-assets.yml deleted file mode 100644 index 93037d6181e..00000000000 --- a/changelogs/unreleased/32992-consider-using-zopfli-over-standard-gzip-compression-for-webpack-assets.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use zopfli compression for frontend assets -merge_request: 11798 -author: diff --git a/changelogs/unreleased/32995-issue-contents-dynamically-replaced-with-stale-version-after-saving-or-refreshing-relative-external_url-only.yml b/changelogs/unreleased/32995-issue-contents-dynamically-replaced-with-stale-version-after-saving-or-refreshing-relative-external_url-only.yml deleted file mode 100644 index 5cd36a4e3e2..00000000000 --- a/changelogs/unreleased/32995-issue-contents-dynamically-replaced-with-stale-version-after-saving-or-refreshing-relative-external_url-only.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix incorrect ETag cache key when relative instance URL is used -merge_request: 11964 -author: diff --git a/changelogs/unreleased/33000-tag-list-in-project-create-api.yml b/changelogs/unreleased/33000-tag-list-in-project-create-api.yml deleted file mode 100644 index b0d0d3cbeba..00000000000 --- a/changelogs/unreleased/33000-tag-list-in-project-create-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add tag_list param to project api -merge_request: 11799 -author: Ivan Chernov diff --git a/changelogs/unreleased/33032-invalid-you-directly-addressed-yourself-todo-when-using-unsubscribe.yml b/changelogs/unreleased/33032-invalid-you-directly-addressed-yourself-todo-when-using-unsubscribe.yml deleted file mode 100644 index 1eaa0d0124e..00000000000 --- a/changelogs/unreleased/33032-invalid-you-directly-addressed-yourself-todo-when-using-unsubscribe.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix /unsubscribe slash command creating extra todos when you were already mentioned - in an issue -merge_request: -author: diff --git a/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml b/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml deleted file mode 100644 index 5648e013e75..00000000000 --- a/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix math rendering on blob pages -merge_request: -author: diff --git a/changelogs/unreleased/33154-permissions-for-project-labels-and-group-labels.yml b/changelogs/unreleased/33154-permissions-for-project-labels-and-group-labels.yml deleted file mode 100644 index 3b98525167d..00000000000 --- a/changelogs/unreleased/33154-permissions-for-project-labels-and-group-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow group reporters to manage group labels -merge_request: -author: diff --git a/changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml b/changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml deleted file mode 100644 index 5eb4e15e311..00000000000 --- a/changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow admins to delete users from the admin users page -merge_request: 11852 -author: diff --git a/changelogs/unreleased/33215-fix-hard-delete-of-users.yml b/changelogs/unreleased/33215-fix-hard-delete-of-users.yml deleted file mode 100644 index 29699ff745a..00000000000 --- a/changelogs/unreleased/33215-fix-hard-delete-of-users.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix hard-deleting users when they have authored issues -merge_request: 11855 -author: diff --git a/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml b/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml deleted file mode 100644 index c33278998ee..00000000000 --- a/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix missing optional path parameter in "Create project for user" API -merge_request: 11868 -author: diff --git a/changelogs/unreleased/33245-chinese_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33245-chinese_translation_of_cycle_analytics_page.yml deleted file mode 100644 index 07dd0872d3b..00000000000 --- a/changelogs/unreleased/33245-chinese_translation_of_cycle_analytics_page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add Chinese translation of Cycle Analytics Page to I18N -merge_request: 11644 -author:Huang Tao diff --git a/changelogs/unreleased/33308-use-pre-wrap-for-commit-messages.yml b/changelogs/unreleased/33308-use-pre-wrap-for-commit-messages.yml deleted file mode 100644 index 43e8f242947..00000000000 --- a/changelogs/unreleased/33308-use-pre-wrap-for-commit-messages.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use pre-wrap for commit messages to keep lists indented -merge_request: -author: diff --git a/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml deleted file mode 100644 index a0e0458da16..00000000000 --- a/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add Portuguese Brazil of Cycle Analytics Page to I18N -merge_request: 11920 -author:Huang Tao diff --git a/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml deleted file mode 100644 index 71bd5505be7..00000000000 --- a/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: add bulgarian translation of cycle analytics page to I18N -merge_request: 11958 -author: Lyubomir Vasilev diff --git a/changelogs/unreleased/33441-supplement_simplified_chinese_translation_of_i18n.yml b/changelogs/unreleased/33441-supplement_simplified_chinese_translation_of_i18n.yml new file mode 100644 index 00000000000..a7d8ac9054b --- /dev/null +++ b/changelogs/unreleased/33441-supplement_simplified_chinese_translation_of_i18n.yml @@ -0,0 +1,4 @@ +--- +title: Supplement Simplified Chinese translation of Project Page & Repository Page +merge_request: 11994 +author: Huang Tao diff --git a/changelogs/unreleased/33442-supplement_traditional_chinese_in_hong_kong_translation_of_i18n.yml b/changelogs/unreleased/33442-supplement_traditional_chinese_in_hong_kong_translation_of_i18n.yml new file mode 100644 index 00000000000..e383bab23d6 --- /dev/null +++ b/changelogs/unreleased/33442-supplement_traditional_chinese_in_hong_kong_translation_of_i18n.yml @@ -0,0 +1,4 @@ +--- +title: Supplement Traditional Chinese in Hong Kong translation of Project Page & Repository Page +merge_request: 11995 +author: Huang Tao diff --git a/changelogs/unreleased/33445-document-delete-merge-branches-won-t-touch-protected-branches-docs.yml b/changelogs/unreleased/33445-document-delete-merge-branches-won-t-touch-protected-branches-docs.yml new file mode 100644 index 00000000000..385f18e2560 --- /dev/null +++ b/changelogs/unreleased/33445-document-delete-merge-branches-won-t-touch-protected-branches-docs.yml @@ -0,0 +1,4 @@ +--- +title: Document the Delete Merged Branches functionality +merge_request: +author: diff --git a/changelogs/unreleased/33461-display-user-id.yml b/changelogs/unreleased/33461-display-user-id.yml new file mode 100644 index 00000000000..cba94625b07 --- /dev/null +++ b/changelogs/unreleased/33461-display-user-id.yml @@ -0,0 +1,4 @@ +--- +title: Display own user id in account settings page +merge_request: 12141 +author: Riccardo Padovani diff --git a/changelogs/unreleased/33538-update-ci-dockerfile-now-that-chrome-headless-no-longer-in-beta.yml b/changelogs/unreleased/33538-update-ci-dockerfile-now-that-chrome-headless-no-longer-in-beta.yml new file mode 100644 index 00000000000..590472c0990 --- /dev/null +++ b/changelogs/unreleased/33538-update-ci-dockerfile-now-that-chrome-headless-no-longer-in-beta.yml @@ -0,0 +1,4 @@ +--- +title: Update QA Dockerfile to lock Chrome browser version +merge_request: 12071 +author: diff --git a/changelogs/unreleased/33561-supplement_bulgarian_translation_of_i18n.yml b/changelogs/unreleased/33561-supplement_bulgarian_translation_of_i18n.yml new file mode 100644 index 00000000000..4f2ba2e1de3 --- /dev/null +++ b/changelogs/unreleased/33561-supplement_bulgarian_translation_of_i18n.yml @@ -0,0 +1,4 @@ +--- +title: Supplement Bulgarian translation of Project Page & Repository Page +merge_request: 12083 +author: Lyubomir Vasilev diff --git a/changelogs/unreleased/33837-remove-trash-on-registry-image.yml b/changelogs/unreleased/33837-remove-trash-on-registry-image.yml new file mode 100644 index 00000000000..2d337f5e6e4 --- /dev/null +++ b/changelogs/unreleased/33837-remove-trash-on-registry-image.yml @@ -0,0 +1,4 @@ +--- +title: Remove registry image delete button if user cant delete it +merge_request: 12317 +author: Ivan Chernov diff --git a/changelogs/unreleased/33846-no-runner-for-admin.yml b/changelogs/unreleased/33846-no-runner-for-admin.yml new file mode 100644 index 00000000000..a2d46802c61 --- /dev/null +++ b/changelogs/unreleased/33846-no-runner-for-admin.yml @@ -0,0 +1,4 @@ +--- +title: Add explicit message when no runners on admin +merge_request: 12266 +author: Takuya Noguchi diff --git a/changelogs/unreleased/34052-store-mr-ref-fetched-in-database.yml b/changelogs/unreleased/34052-store-mr-ref-fetched-in-database.yml new file mode 100644 index 00000000000..4bacfca7551 --- /dev/null +++ b/changelogs/unreleased/34052-store-mr-ref-fetched-in-database.yml @@ -0,0 +1,4 @@ +--- +title: Store merge request ref_fetched status in the database +merge_request: 12424 +author: diff --git a/changelogs/unreleased/34207-remove-bin-ci-upgrade-rb.yml b/changelogs/unreleased/34207-remove-bin-ci-upgrade-rb.yml new file mode 100644 index 00000000000..4fa385c3c27 --- /dev/null +++ b/changelogs/unreleased/34207-remove-bin-ci-upgrade-rb.yml @@ -0,0 +1,4 @@ +--- +title: Remove bin/ci/upgrade.rb as not working all +merge_request: 12414 +author: Takuya Noguchi diff --git a/changelogs/unreleased/34286-add-esperanto-translations-for-cycle-analytics-and-project-and-repository-pages.yml b/changelogs/unreleased/34286-add-esperanto-translations-for-cycle-analytics-and-project-and-repository-pages.yml new file mode 100644 index 00000000000..af743f3e506 --- /dev/null +++ b/changelogs/unreleased/34286-add-esperanto-translations-for-cycle-analytics-and-project-and-repository-pages.yml @@ -0,0 +1,4 @@ +--- +title: Add Esperanto translations for Cycle Analytics, Project, and Repository pages +merge_request: 12442 +author: Huang Tao diff --git a/changelogs/unreleased/34289-drop-gfm-on-milestone-issuable-title.yml b/changelogs/unreleased/34289-drop-gfm-on-milestone-issuable-title.yml new file mode 100644 index 00000000000..42e906d24c6 --- /dev/null +++ b/changelogs/unreleased/34289-drop-gfm-on-milestone-issuable-title.yml @@ -0,0 +1,4 @@ +--- +title: Drop GFM support for issuable title on milestone for consistency and performance +merge_request: +author: Takuya Noguchi diff --git a/changelogs/unreleased/34309-drop-gfm-mr-ms.yml b/changelogs/unreleased/34309-drop-gfm-mr-ms.yml new file mode 100644 index 00000000000..07fe79e90ee --- /dev/null +++ b/changelogs/unreleased/34309-drop-gfm-mr-ms.yml @@ -0,0 +1,4 @@ +--- +title: Drop GFM support for the title of Milestone/MergeRequest in template +merge_request: 12451 +author: Takuya Noguchi diff --git a/changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml b/changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml deleted file mode 100644 index 374f643faa7..00000000000 --- a/changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Count badges depend on translucent color to better adjust to different background - colors and permission badges now feature a pill shaped design similar to labels -merge_request: -author: diff --git a/changelogs/unreleased/adam-influxdb-hostname.yml b/changelogs/unreleased/adam-influxdb-hostname.yml deleted file mode 100644 index ab201ae7894..00000000000 --- a/changelogs/unreleased/adam-influxdb-hostname.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow GitLab instance to start when InfluxDB hostname cannot be resolved -merge_request: 11356 -author: diff --git a/changelogs/unreleased/add-index-for-auto_canceled_by_id-mysql.yml b/changelogs/unreleased/add-index-for-auto_canceled_by_id-mysql.yml deleted file mode 100644 index eac78e9ee1f..00000000000 --- a/changelogs/unreleased/add-index-for-auto_canceled_by_id-mysql.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add indices for auto_canceled_by_id for ci_pipelines and ci_builds on PostgreSQL -merge_request: 11034 -author: diff --git a/changelogs/unreleased/add-unicode-trace-feature-test.yml b/changelogs/unreleased/add-unicode-trace-feature-test.yml deleted file mode 100644 index 90c6a9afefc..00000000000 --- a/changelogs/unreleased/add-unicode-trace-feature-test.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add a feature test for Unicode trace -merge_request: 10736 -author: dosuken123 diff --git a/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml b/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml deleted file mode 100644 index fcf4efa2846..00000000000 --- a/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add an ability to cancel attaching file and redesign attaching files UI -merge_request: 9431 -author: blackst0ne diff --git a/changelogs/unreleased/aliyun-backup-provider.yml b/changelogs/unreleased/aliyun-backup-provider.yml deleted file mode 100644 index e7505e44a59..00000000000 --- a/changelogs/unreleased/aliyun-backup-provider.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add Aliyun OSS as the backup storage provider -merge_request: 9721 -author: Yuanfei Zhu diff --git a/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml b/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml deleted file mode 100644 index 2364ce6d068..00000000000 --- a/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow reporters to promote project labels to group labels -merge_request: -author: diff --git a/changelogs/unreleased/allow_numeric_pages_domain.yml b/changelogs/unreleased/allow_numeric_pages_domain.yml deleted file mode 100644 index 10d9f26f88d..00000000000 --- a/changelogs/unreleased/allow_numeric_pages_domain.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow numeric pages domain -merge_request: 11550 -author: diff --git a/changelogs/unreleased/allow_numeric_values_in_gitlab_ci_yml.yml b/changelogs/unreleased/allow_numeric_values_in_gitlab_ci_yml.yml deleted file mode 100644 index 8c7fa53a18b..00000000000 --- a/changelogs/unreleased/allow_numeric_values_in_gitlab_ci_yml.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow numeric values in gitlab-ci.yml -merge_request: 10607 -author: blackst0ne diff --git a/changelogs/unreleased/artifacts-keyboard-shortcuts.yml b/changelogs/unreleased/artifacts-keyboard-shortcuts.yml deleted file mode 100644 index 69569504c4f..00000000000 --- a/changelogs/unreleased/artifacts-keyboard-shortcuts.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enabled keyboard shortcuts on artifacts pages -merge_request: -author: diff --git a/changelogs/unreleased/auto-search-when-state-changed.yml b/changelogs/unreleased/auto-search-when-state-changed.yml deleted file mode 100644 index 2723beb8600..00000000000 --- a/changelogs/unreleased/auto-search-when-state-changed.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Perform filtered search when state tab is changed -merge_request: -author: diff --git a/changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml b/changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml deleted file mode 100644 index 0306663ac8d..00000000000 --- a/changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Fixed handling of the `can_push` attribute in the v3 deploy_keys api" -merge_request: 11607 -author: Richard Clamp diff --git a/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml b/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml deleted file mode 100644 index 2ce01a71361..00000000000 --- a/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rename build_events to job_events -merge_request: 11287 -author: diff --git a/changelogs/unreleased/bvl-translate-project-pages.yml b/changelogs/unreleased/bvl-translate-project-pages.yml deleted file mode 100644 index fb90aba08b4..00000000000 --- a/changelogs/unreleased/bvl-translate-project-pages.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Translate backend for Project & Repository pages -merge_request: 11183 -author: diff --git a/changelogs/unreleased/ce-31853-projects-shared-groups.yml b/changelogs/unreleased/ce-31853-projects-shared-groups.yml deleted file mode 100644 index ffa3aed682d..00000000000 --- a/changelogs/unreleased/ce-31853-projects-shared-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove duplication for sharing projects with groups in project settings -merge_request: -author: diff --git a/changelogs/unreleased/ce-32623-browser-tooltip-commits-branch-list.yml b/changelogs/unreleased/ce-32623-browser-tooltip-commits-branch-list.yml deleted file mode 100644 index 93edafed699..00000000000 --- a/changelogs/unreleased/ce-32623-browser-tooltip-commits-branch-list.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Change order of commits ahead and behind on divergence graph for branch list - view -merge_request: -author: diff --git a/changelogs/unreleased/ci-build-pipeline-header-vue.yml b/changelogs/unreleased/ci-build-pipeline-header-vue.yml deleted file mode 100644 index 2bbff2fdd16..00000000000 --- a/changelogs/unreleased/ci-build-pipeline-header-vue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Creates CI Header component for Pipelines and Jobs details pages -merge_request: -author: diff --git a/changelogs/unreleased/commit-comments-limited-width.yml b/changelogs/unreleased/commit-comments-limited-width.yml new file mode 100644 index 00000000000..97f50105495 --- /dev/null +++ b/changelogs/unreleased/commit-comments-limited-width.yml @@ -0,0 +1,4 @@ +--- +title: Limit commit & snippets comments width +merge_request: +author: diff --git a/changelogs/unreleased/counters_cache_invalidation.yml b/changelogs/unreleased/counters_cache_invalidation.yml deleted file mode 100644 index 1e78765ec10..00000000000 --- a/changelogs/unreleased/counters_cache_invalidation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Invalidate cache for issue and MR counters more granularly -merge_request: -author: diff --git a/changelogs/unreleased/dm-async-tree-readme.yml b/changelogs/unreleased/dm-async-tree-readme.yml deleted file mode 100644 index fb1cfeb210a..00000000000 --- a/changelogs/unreleased/dm-async-tree-readme.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Load tree readme asynchronously -merge_request: -author: diff --git a/changelogs/unreleased/dm-auxiliary-viewers.yml b/changelogs/unreleased/dm-auxiliary-viewers.yml deleted file mode 100644 index ba73a499115..00000000000 --- a/changelogs/unreleased/dm-auxiliary-viewers.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and - LICENSE blob pages -merge_request: -author: diff --git a/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml b/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml deleted file mode 100644 index 50db66c89ba..00000000000 --- a/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix replying to a commit discussion displayed in the context of an MR -merge_request: -author: diff --git a/changelogs/unreleased/dm-commit-row-browse-button.yml b/changelogs/unreleased/dm-commit-row-browse-button.yml new file mode 100644 index 00000000000..4240a7de5de --- /dev/null +++ b/changelogs/unreleased/dm-commit-row-browse-button.yml @@ -0,0 +1,4 @@ +--- +title: Fix inconsistent display of the "Browse files" button in the commit list +merge_request: +author: diff --git a/changelogs/unreleased/dm-consistent-commit-sha-style.yml b/changelogs/unreleased/dm-consistent-commit-sha-style.yml deleted file mode 100644 index b6dace34d9b..00000000000 --- a/changelogs/unreleased/dm-consistent-commit-sha-style.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Consistently use monospace font for commit SHAs and branch and tag names -merge_request: -author: diff --git a/changelogs/unreleased/dm-consistent-last-push-event.yml b/changelogs/unreleased/dm-consistent-last-push-event.yml deleted file mode 100644 index acc17cb4523..00000000000 --- a/changelogs/unreleased/dm-consistent-last-push-event.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Consistently display last push event widget -merge_request: -author: diff --git a/changelogs/unreleased/dm-copy-as-gfm-without-empty-elements.yml b/changelogs/unreleased/dm-copy-as-gfm-without-empty-elements.yml deleted file mode 100644 index 45a61320ff2..00000000000 --- a/changelogs/unreleased/dm-copy-as-gfm-without-empty-elements.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't copy empty elements that were not selected on purpose as GFM -merge_request: -author: diff --git a/changelogs/unreleased/dm-copy-gfm-when-parts-of-other-elements-are-selected.yml b/changelogs/unreleased/dm-copy-gfm-when-parts-of-other-elements-are-selected.yml deleted file mode 100644 index ae916c30ff8..00000000000 --- a/changelogs/unreleased/dm-copy-gfm-when-parts-of-other-elements-are-selected.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Copy as GFM even when parts of other elements are selected -merge_request: -author: diff --git a/changelogs/unreleased/dm-dependency-linker-gemfile.yml b/changelogs/unreleased/dm-dependency-linker-gemfile.yml deleted file mode 100644 index 2d4167a1be5..00000000000 --- a/changelogs/unreleased/dm-dependency-linker-gemfile.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Autolink package names in Gemfile -merge_request: -author: diff --git a/changelogs/unreleased/dm-diff-viewers.yml b/changelogs/unreleased/dm-diff-viewers.yml new file mode 100644 index 00000000000..e5b1352c8f1 --- /dev/null +++ b/changelogs/unreleased/dm-diff-viewers.yml @@ -0,0 +1,4 @@ +--- +title: Implement diff viewers +merge_request: +author: diff --git a/changelogs/unreleased/dm-discussions-n-plus-1.yml b/changelogs/unreleased/dm-discussions-n-plus-1.yml deleted file mode 100644 index b97e4344248..00000000000 --- a/changelogs/unreleased/dm-discussions-n-plus-1.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Resolve N+1 query issue with discussions -merge_request: -author: diff --git a/changelogs/unreleased/dm-emails-are-not-user-references.yml b/changelogs/unreleased/dm-emails-are-not-user-references.yml deleted file mode 100644 index fe55a75a88f..00000000000 --- a/changelogs/unreleased/dm-emails-are-not-user-references.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't match email addresses or foo@bar as user references -merge_request: -author: diff --git a/changelogs/unreleased/dm-fix-jump-button.yml b/changelogs/unreleased/dm-fix-jump-button.yml deleted file mode 100644 index 4cde354fa28..00000000000 --- a/changelogs/unreleased/dm-fix-jump-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix title of discussion jump button at top of page -merge_request: -author: diff --git a/changelogs/unreleased/dm-gitmodules-parsing.yml b/changelogs/unreleased/dm-gitmodules-parsing.yml deleted file mode 100644 index a7d755d6c4d..00000000000 --- a/changelogs/unreleased/dm-gitmodules-parsing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make .gitmodules parsing more resilient to syntax errors -merge_request: -author: diff --git a/changelogs/unreleased/dm-gravatar-username.yml b/changelogs/unreleased/dm-gravatar-username.yml deleted file mode 100644 index d50455061ec..00000000000 --- a/changelogs/unreleased/dm-gravatar-username.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add username parameter to gravatar URL -merge_request: -author: diff --git a/changelogs/unreleased/dm-group-page-name.yml b/changelogs/unreleased/dm-group-page-name.yml new file mode 100644 index 00000000000..233879364e3 --- /dev/null +++ b/changelogs/unreleased/dm-group-page-name.yml @@ -0,0 +1,4 @@ +--- +title: Show group name instead of path on group page +merge_request: +author: diff --git a/changelogs/unreleased/dm-more-dependency-linkers.yml b/changelogs/unreleased/dm-more-dependency-linkers.yml deleted file mode 100644 index 12d45e71e85..00000000000 --- a/changelogs/unreleased/dm-more-dependency-linkers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Autolink package names in more dependency files -merge_request: -author: diff --git a/changelogs/unreleased/dm-oauth-config-for.yml b/changelogs/unreleased/dm-oauth-config-for.yml deleted file mode 100644 index 8fbbd45bb57..00000000000 --- a/changelogs/unreleased/dm-oauth-config-for.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Return nil when looking up config for unknown LDAP provider -merge_request: -author: diff --git a/changelogs/unreleased/dm-outdated-system-note.yml b/changelogs/unreleased/dm-outdated-system-note.yml deleted file mode 100644 index a1038a1051b..00000000000 --- a/changelogs/unreleased/dm-outdated-system-note.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add system note with link to diff comparison when MR discussion becomes outdated -merge_request: -author: diff --git a/changelogs/unreleased/dm-paste-code-inside-gfm-code.yml b/changelogs/unreleased/dm-paste-code-inside-gfm-code.yml deleted file mode 100644 index d078ca449a5..00000000000 --- a/changelogs/unreleased/dm-paste-code-inside-gfm-code.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't wrap pasted code when it's already inside code tags -merge_request: -author: diff --git a/changelogs/unreleased/dm-revert-mr-8427.yml b/changelogs/unreleased/dm-revert-mr-8427.yml deleted file mode 100644 index a91cff2e9cd..00000000000 --- a/changelogs/unreleased/dm-revert-mr-8427.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Revert 'New file from interface on existing branch' -merge_request: -author: diff --git a/changelogs/unreleased/dm-target-branch-slash-command-desc.yml b/changelogs/unreleased/dm-target-branch-slash-command-desc.yml new file mode 100644 index 00000000000..768ddf0416e --- /dev/null +++ b/changelogs/unreleased/dm-target-branch-slash-command-desc.yml @@ -0,0 +1,4 @@ +--- +title: Update /target_branch slash command description to be more consistent +merge_request: +author: diff --git a/changelogs/unreleased/dm-tree-last-commit.yml b/changelogs/unreleased/dm-tree-last-commit.yml deleted file mode 100644 index 50619fd6ef2..00000000000 --- a/changelogs/unreleased/dm-tree-last-commit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show last commit for current tree on tree page -merge_request: -author: diff --git a/changelogs/unreleased/dm-unnecessary-top-padding.yml b/changelogs/unreleased/dm-unnecessary-top-padding.yml new file mode 100644 index 00000000000..4557c06f8e7 --- /dev/null +++ b/changelogs/unreleased/dm-unnecessary-top-padding.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary top padding on group MR index +merge_request: +author: diff --git a/changelogs/unreleased/doc-gitaly-network.yml b/changelogs/unreleased/doc-gitaly-network.yml new file mode 100644 index 00000000000..5376d8d5096 --- /dev/null +++ b/changelogs/unreleased/doc-gitaly-network.yml @@ -0,0 +1,4 @@ +--- +title: Add option to run Gitaly on a remote server +merge_request: 12381 +author: diff --git a/changelogs/unreleased/document-foreign-keys.yml b/changelogs/unreleased/document-foreign-keys.yml deleted file mode 100644 index faa467e8185..00000000000 --- a/changelogs/unreleased/document-foreign-keys.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add documentation about adding foreign keys -merge_request: -author: diff --git a/changelogs/unreleased/dt-printing-to-api.yml b/changelogs/unreleased/dt-printing-to-api.yml new file mode 100644 index 00000000000..5253b57f21a --- /dev/null +++ b/changelogs/unreleased/dt-printing-to-api.yml @@ -0,0 +1,4 @@ +--- +title: Added printing_merge_requst_link_enabled to the API +merge_request: +author: David Turner <dturner@twosigma.com> diff --git a/changelogs/unreleased/dturner-username.yml b/changelogs/unreleased/dturner-username.yml deleted file mode 100644 index 09ba822ee65..00000000000 --- a/changelogs/unreleased/dturner-username.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: add username field to push webhook -merge_request: -author: David Turner diff --git a/changelogs/unreleased/dz-fix-submodule-subgroup.yml b/changelogs/unreleased/dz-fix-submodule-subgroup.yml deleted file mode 100644 index 20c7c9ce657..00000000000 --- a/changelogs/unreleased/dz-fix-submodule-subgroup.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix submodule link to then project under subgroup -merge_request: 11906 -author: diff --git a/changelogs/unreleased/dz-project-list-cache-key.yml b/changelogs/unreleased/dz-project-list-cache-key.yml deleted file mode 100644 index 9e4826e686a..00000000000 --- a/changelogs/unreleased/dz-project-list-cache-key.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use route.cache_key for project list cache key -merge_request: 11325 -author: diff --git a/changelogs/unreleased/dz-rename-pipelines-settings-tab.yml b/changelogs/unreleased/dz-rename-pipelines-settings-tab.yml deleted file mode 100644 index 6a1232523bb..00000000000 --- a/changelogs/unreleased/dz-rename-pipelines-settings-tab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rename CI/CD Pipelines to Pipelines in the project settings -merge_request: -author: diff --git a/changelogs/unreleased/enable-auto-cancelling-by-default.yml b/changelogs/unreleased/enable-auto-cancelling-by-default.yml deleted file mode 100644 index 8b1659bf38b..00000000000 --- a/changelogs/unreleased/enable-auto-cancelling-by-default.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enable cancelling non-HEAD pending pipelines by default for all projects -merge_request: 11023 -author: diff --git a/changelogs/unreleased/environment-detail-view.yml b/changelogs/unreleased/environment-detail-view.yml deleted file mode 100644 index c74f70ea86d..00000000000 --- a/changelogs/unreleased/environment-detail-view.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make environment tables responsive -merge_request: -author: diff --git a/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml b/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml deleted file mode 100644 index 4796f8e918b..00000000000 --- a/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Expand/collapse backlog & closed lists in issue boards -merge_request: -author: diff --git a/changelogs/unreleased/feature-add-support-for-services-configuration.yml b/changelogs/unreleased/feature-add-support-for-services-configuration.yml new file mode 100644 index 00000000000..88a3eacd774 --- /dev/null +++ b/changelogs/unreleased/feature-add-support-for-services-configuration.yml @@ -0,0 +1,4 @@ +--- +title: Add support for image and services configuration in .gitlab-ci.yml +merge_request: 8578 +author: diff --git a/changelogs/unreleased/feature-flags-flipper.yml b/changelogs/unreleased/feature-flags-flipper.yml deleted file mode 100644 index 5be5c44166d..00000000000 --- a/changelogs/unreleased/feature-flags-flipper.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add feature toggles and API endpoints for admins -merge_request: 11747 -author: diff --git a/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml b/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml deleted file mode 100644 index 1404b342359..00000000000 --- a/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Persist pipeline stages in the database -merge_request: 11790 -author: diff --git a/changelogs/unreleased/feature-print-go-version-in-env-info.yml b/changelogs/unreleased/feature-print-go-version-in-env-info.yml deleted file mode 100644 index 34c19b06eda..00000000000 --- a/changelogs/unreleased/feature-print-go-version-in-env-info.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Print Go version in rake gitlab:env:info -merge_request: 11241 -author: diff --git a/changelogs/unreleased/feature-rss-scoped-token.yml b/changelogs/unreleased/feature-rss-scoped-token.yml deleted file mode 100644 index 740d8778be2..00000000000 --- a/changelogs/unreleased/feature-rss-scoped-token.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Expose atom links with an RSS token instead of using the private token -merge_request: 11647 -author: Alexis Reigel diff --git a/changelogs/unreleased/feature-unify-email-layouts.yml b/changelogs/unreleased/feature-unify-email-layouts.yml new file mode 100644 index 00000000000..7a2e3f20b6b --- /dev/null +++ b/changelogs/unreleased/feature-unify-email-layouts.yml @@ -0,0 +1,4 @@ +--- +title: Update the devise mail templates to match the design of the pipeline emails +merge_request: 10483 +author: Alexis Reigel diff --git a/changelogs/unreleased/fix-33991.yml b/changelogs/unreleased/fix-33991.yml new file mode 100644 index 00000000000..39732611b6e --- /dev/null +++ b/changelogs/unreleased/fix-33991.yml @@ -0,0 +1,4 @@ +--- +title: Users can subscribe to group labels on the group labels page +merge_request: +author: diff --git a/changelogs/unreleased/fix-backup-restore-resume.yml b/changelogs/unreleased/fix-backup-restore-resume.yml deleted file mode 100644 index b7dfd451f5d..00000000000 --- a/changelogs/unreleased/fix-backup-restore-resume.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make backup task to continue on corrupt repositories -merge_request: 11962 -author: diff --git a/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml b/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml deleted file mode 100644 index e40668546c0..00000000000 --- a/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix counter cache for acts as taggable -merge_request: -author: diff --git a/changelogs/unreleased/fix-encoding-binary-issue.yml b/changelogs/unreleased/fix-encoding-binary-issue.yml deleted file mode 100644 index ac9aff64a88..00000000000 --- a/changelogs/unreleased/fix-encoding-binary-issue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix binary encoding error on MR diffs -merge_request: 11929 -author: diff --git a/changelogs/unreleased/fix-gb-exclude-manual-actions-from-cancelable-jobs.yml b/changelogs/unreleased/fix-gb-exclude-manual-actions-from-cancelable-jobs.yml deleted file mode 100644 index a16fc775b5e..00000000000 --- a/changelogs/unreleased/fix-gb-exclude-manual-actions-from-cancelable-jobs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Exclude manual actions when checking if pipeline can be canceled -merge_request: 11562 -author: diff --git a/changelogs/unreleased/fix-gb-fix-skipped-pipeline-with-allowed-to-fail-jobs.yml b/changelogs/unreleased/fix-gb-fix-skipped-pipeline-with-allowed-to-fail-jobs.yml new file mode 100644 index 00000000000..f59c6ecd90c --- /dev/null +++ b/changelogs/unreleased/fix-gb-fix-skipped-pipeline-with-allowed-to-fail-jobs.yml @@ -0,0 +1,4 @@ +--- +title: Fix CI/CD status in case there are only allowed to failed jobs in the pipeline +merge_request: 11166 +author: diff --git a/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml b/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml deleted file mode 100644 index 43c18502cd6..00000000000 --- a/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Respect merge, instead of push, permissions for protected actions -merge_request: 11648 -author: diff --git a/changelogs/unreleased/fix-github-clone-wiki.yml b/changelogs/unreleased/fix-github-clone-wiki.yml deleted file mode 100644 index eadd90e1390..00000000000 --- a/changelogs/unreleased/fix-github-clone-wiki.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Github - Fix token interpolation when cloning wiki repository -merge_request: -author: diff --git a/changelogs/unreleased/fix-github-import.yml b/changelogs/unreleased/fix-github-import.yml deleted file mode 100644 index 3a57152f7a8..00000000000 --- a/changelogs/unreleased/fix-github-import.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix token interpolation when setting the Github remote -merge_request: -author: diff --git a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml b/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml new file mode 100644 index 00000000000..f12e7b53790 --- /dev/null +++ b/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml @@ -0,0 +1,4 @@ +--- +title: Fix head pipeline stored in merge request for external pipelines +merge_request: 12478 +author: diff --git a/changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml b/changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml deleted file mode 100644 index c2671a96b83..00000000000 --- a/changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix N+1 queries for non-members in comment threads -merge_request: -author: diff --git a/changelogs/unreleased/fix-overflow-slash-commands.yml b/changelogs/unreleased/fix-overflow-slash-commands.yml new file mode 100644 index 00000000000..98ec399e8cb --- /dev/null +++ b/changelogs/unreleased/fix-overflow-slash-commands.yml @@ -0,0 +1,4 @@ +--- +title: Fixed overflow on mobile screens for the slash commands +merge_request: +author: diff --git a/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml b/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml deleted file mode 100644 index fb91da9510c..00000000000 --- a/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix terminals support for Kubernetes Service -merge_request: -author: diff --git a/changelogs/unreleased/fix_commits_page.yml b/changelogs/unreleased/fix_commits_page.yml deleted file mode 100644 index a2afaf6e626..00000000000 --- a/changelogs/unreleased/fix_commits_page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix duplication of commits header on commits page -merge_request: 11006 -author: @blackst0ne diff --git a/changelogs/unreleased/fix_diff_line_comments.yml b/changelogs/unreleased/fix_diff_line_comments.yml deleted file mode 100644 index bdb0539b49d..00000000000 --- a/changelogs/unreleased/fix_diff_line_comments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix: A diff comment on a change at last line of a file shows as two comments - in discussion' -merge_request: -author: diff --git a/changelogs/unreleased/fixed-confidential-issue-bar.yml b/changelogs/unreleased/fixed-confidential-issue-bar.yml deleted file mode 100644 index 6a41590d0af..00000000000 --- a/changelogs/unreleased/fixed-confidential-issue-bar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make confidential issues more obviously confidential -merge_request: -author: diff --git a/changelogs/unreleased/gitaly-local-branches.yml b/changelogs/unreleased/gitaly-local-branches.yml deleted file mode 100644 index adcc0fa6280..00000000000 --- a/changelogs/unreleased/gitaly-local-branches.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add suport for find_local_branches GRPC from Gitaly -merge_request: 10059 -author: diff --git a/changelogs/unreleased/gitaly-opt-out.yml b/changelogs/unreleased/gitaly-opt-out.yml deleted file mode 100644 index 2f89e0bfc9a..00000000000 --- a/changelogs/unreleased/gitaly-opt-out.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enable Gitaly by default in installations from source -merge_request: 11796 -author: diff --git a/changelogs/unreleased/help-landing-page-customizations.yml b/changelogs/unreleased/help-landing-page-customizations.yml new file mode 100644 index 00000000000..58cab751ded --- /dev/null +++ b/changelogs/unreleased/help-landing-page-customizations.yml @@ -0,0 +1,4 @@ +--- +title: Help landing page customizations +merge_request: 11878 +author: Robin Bobbitt diff --git a/changelogs/unreleased/introduce-source-to-pipelines.yml b/changelogs/unreleased/introduce-source-to-pipelines.yml deleted file mode 100644 index 7898bd31b39..00000000000 --- a/changelogs/unreleased/introduce-source-to-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce source to Pipeline entity -merge_request: -author: diff --git a/changelogs/unreleased/issuable-form-create-label-sub-groups.yml b/changelogs/unreleased/issuable-form-create-label-sub-groups.yml deleted file mode 100644 index 54b818d6d5e..00000000000 --- a/changelogs/unreleased/issuable-form-create-label-sub-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed create new label form in issue form not working for sub-group projects -merge_request: -author: diff --git a/changelogs/unreleased/issue-23254.yml b/changelogs/unreleased/issue-23254.yml deleted file mode 100644 index 568a7a41c30..00000000000 --- a/changelogs/unreleased/issue-23254.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed style on unsubscribe page -merge_request: -author: Gustav Ernberg diff --git a/changelogs/unreleased/issue-boards-closed-list-all.yml b/changelogs/unreleased/issue-boards-closed-list-all.yml new file mode 100644 index 00000000000..7643864150d --- /dev/null +++ b/changelogs/unreleased/issue-boards-closed-list-all.yml @@ -0,0 +1,4 @@ +--- +title: Fixed issue boards closed list not showing all closed issues +merge_request: +author: diff --git a/changelogs/unreleased/issue-edit-inline.yml b/changelogs/unreleased/issue-edit-inline.yml deleted file mode 100644 index db03d1bdac4..00000000000 --- a/changelogs/unreleased/issue-edit-inline.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enables inline editing for an issues title & description -merge_request: -author: diff --git a/changelogs/unreleased/issue-form-multiple-line-markdown.yml b/changelogs/unreleased/issue-form-multiple-line-markdown.yml new file mode 100644 index 00000000000..23128f346bc --- /dev/null +++ b/changelogs/unreleased/issue-form-multiple-line-markdown.yml @@ -0,0 +1,4 @@ +--- +title: Fixed multi-line markdown tooltip buttons in issue edit form +merge_request: +author: diff --git a/changelogs/unreleased/issue-template-reproduce-in-example-project.yml b/changelogs/unreleased/issue-template-reproduce-in-example-project.yml deleted file mode 100644 index 8116007b459..00000000000 --- a/changelogs/unreleased/issue-template-reproduce-in-example-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ask for an example project for bug reports -merge_request: -author: diff --git a/changelogs/unreleased/issue-templates-summary-lines.yml b/changelogs/unreleased/issue-templates-summary-lines.yml deleted file mode 100644 index 0c8c3d884ce..00000000000 --- a/changelogs/unreleased/issue-templates-summary-lines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add summary lines for collapsed details in the bug report template -merge_request: -author: diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml deleted file mode 100644 index 7bcbc647fcb..00000000000 --- a/changelogs/unreleased/issue_19262.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent commits from upstream repositories to be re-processed by forks -merge_request: -author: diff --git a/changelogs/unreleased/issue_20900.yml b/changelogs/unreleased/issue_20900.yml new file mode 100644 index 00000000000..e8cef6d2bce --- /dev/null +++ b/changelogs/unreleased/issue_20900.yml @@ -0,0 +1,4 @@ +--- +title: Remove issues/merge requests drag n drop and sorting from milestone view +merge_request: +author: diff --git a/changelogs/unreleased/issue_27166_2.yml b/changelogs/unreleased/issue_27166_2.yml deleted file mode 100644 index 9b9906e03dd..00000000000 --- a/changelogs/unreleased/issue_27166_2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Avoid repeated queries for pipeline builds on merge requests -merge_request: -author: diff --git a/changelogs/unreleased/issue_27168_2.yml b/changelogs/unreleased/issue_27168_2.yml deleted file mode 100644 index c67692493e0..00000000000 --- a/changelogs/unreleased/issue_27168_2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Preloads head pipeline for merge request collection -merge_request: -author: diff --git a/changelogs/unreleased/issue_32225_2.yml b/changelogs/unreleased/issue_32225_2.yml deleted file mode 100644 index 320b9fe00b8..00000000000 --- a/changelogs/unreleased/issue_32225_2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Handle head pipeline when creating merge requests -merge_request: -author: diff --git a/changelogs/unreleased/issue_33205.yml b/changelogs/unreleased/issue_33205.yml new file mode 100644 index 00000000000..54b442048d8 --- /dev/null +++ b/changelogs/unreleased/issue_33205.yml @@ -0,0 +1,4 @@ +--- +title: Fix API bug accepting wrong parameter to create merge request +merge_request: +author: diff --git a/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml b/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml deleted file mode 100644 index df4de9f4e21..00000000000 --- a/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Redirect to user's keys index instead of user's index after a key is deleted - in the admin -merge_request: 10227 -author: Cyril Jouve diff --git a/changelogs/unreleased/karma-headless-chrome.yml b/changelogs/unreleased/karma-headless-chrome.yml new file mode 100644 index 00000000000..af3e9b3b0f9 --- /dev/null +++ b/changelogs/unreleased/karma-headless-chrome.yml @@ -0,0 +1,4 @@ +--- +title: Replace PhantomJS with headless Chrome for karma test suite +merge_request: 12036 +author: diff --git a/changelogs/unreleased/mabes-gitlab-ce-bypass-auto-login.yml b/changelogs/unreleased/mabes-gitlab-ce-bypass-auto-login.yml deleted file mode 100644 index a321ed9d7d8..00000000000 --- a/changelogs/unreleased/mabes-gitlab-ce-bypass-auto-login.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow manual bypass of auto_sign_in_with_provider with a new param -merge_request: 10187 -author: Maxime Besson diff --git a/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml b/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml deleted file mode 100644 index bd022a3a91b..00000000000 --- a/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Migrate artifacts to a new path -merge_request: -author: diff --git a/changelogs/unreleased/mk-fix-git-over-http-rejections.yml b/changelogs/unreleased/mk-fix-git-over-http-rejections.yml deleted file mode 100644 index e75740e913f..00000000000 --- a/changelogs/unreleased/mk-fix-git-over-http-rejections.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Git-over-HTTP error statuses and improve error messages -merge_request: 11398 -author: diff --git a/changelogs/unreleased/moved-submodules.yml b/changelogs/unreleased/moved-submodules.yml new file mode 100644 index 00000000000..eee858717ed --- /dev/null +++ b/changelogs/unreleased/moved-submodules.yml @@ -0,0 +1,4 @@ +--- +title: 'Handle renamed submodules in repository browser' +merge_request: 10798 +author: David Turner diff --git a/changelogs/unreleased/mr-widget-memory-usage-tech-debt-fix.yml b/changelogs/unreleased/mr-widget-memory-usage-tech-debt-fix.yml new file mode 100644 index 00000000000..14b5493a246 --- /dev/null +++ b/changelogs/unreleased/mr-widget-memory-usage-tech-debt-fix.yml @@ -0,0 +1,4 @@ +--- +title: Changed utilities imports from ~ to relative paths +merge_request: +author: diff --git a/changelogs/unreleased/mrchrisw-catch-openssl.yml b/changelogs/unreleased/mrchrisw-catch-openssl.yml deleted file mode 100644 index a8b433fb0cd..00000000000 --- a/changelogs/unreleased/mrchrisw-catch-openssl.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rescue OpenSSL::SSL::SSLError in JiraService & IssueTrackerService -merge_request: -author: diff --git a/changelogs/unreleased/omega-submodules.yml b/changelogs/unreleased/omega-submodules.yml deleted file mode 100644 index 1488eb72174..00000000000 --- a/changelogs/unreleased/omega-submodules.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Repository browser: handle in-repository submodule urls' -merge_request: -author: David Turner diff --git a/changelogs/unreleased/pat-alert-when-signin-disabled.yml b/changelogs/unreleased/pat-alert-when-signin-disabled.yml new file mode 100644 index 00000000000..dca3670aeb7 --- /dev/null +++ b/changelogs/unreleased/pat-alert-when-signin-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Provide hint to create a personal access token for Git over HTTP +merge_request: 12105 +author: Robin Bobbitt diff --git a/changelogs/unreleased/polish-sidebar-toggle.yml b/changelogs/unreleased/polish-sidebar-toggle.yml new file mode 100644 index 00000000000..41ec567fc52 --- /dev/null +++ b/changelogs/unreleased/polish-sidebar-toggle.yml @@ -0,0 +1,4 @@ +--- +title: Remove unused space in sidebar todo toggle when not signed in +merge_request: +author: diff --git a/changelogs/unreleased/prevent-project-transfer.yml b/changelogs/unreleased/prevent-project-transfer.yml deleted file mode 100644 index a5c74676aab..00000000000 --- a/changelogs/unreleased/prevent-project-transfer.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent project transfers if a new group is not selected -merge_request: -author: diff --git a/changelogs/unreleased/project-readme-limited-width.yml b/changelogs/unreleased/project-readme-limited-width.yml new file mode 100644 index 00000000000..17d87a5691e --- /dev/null +++ b/changelogs/unreleased/project-readme-limited-width.yml @@ -0,0 +1,4 @@ +--- +title: Limit the width of the projects README text +merge_request: +author: diff --git a/changelogs/unreleased/projects-api-import-status.yml b/changelogs/unreleased/projects-api-import-status.yml deleted file mode 100644 index 06603c0adec..00000000000 --- a/changelogs/unreleased/projects-api-import-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Expose import_status in Projects API -merge_request: 11851 -author: Robin Bobbitt diff --git a/changelogs/unreleased/protected-branches-no-one-merge.yml b/changelogs/unreleased/protected-branches-no-one-merge.yml deleted file mode 100644 index 52d93793f3d..00000000000 --- a/changelogs/unreleased/protected-branches-no-one-merge.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow 'no one' as an option for allowed to merge on a procted branch -merge_request: -author: diff --git a/changelogs/unreleased/remove-old-isobject.yml b/changelogs/unreleased/remove-old-isobject.yml deleted file mode 100644 index 67b18642253..00000000000 --- a/changelogs/unreleased/remove-old-isobject.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unused code and uses underscore -merge_request: -author: diff --git a/changelogs/unreleased/rename-builds-controller.yml b/changelogs/unreleased/rename-builds-controller.yml deleted file mode 100644 index 7f6872ccf95..00000000000 --- a/changelogs/unreleased/rename-builds-controller.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change /builds in the URL to /-/jobs. Backward URLs were also added -merge_request: 11407 -author: diff --git a/changelogs/unreleased/replace_spinach_spec_profile_notifications-feature.yml b/changelogs/unreleased/replace_spinach_spec_profile_notifications-feature.yml new file mode 100644 index 00000000000..38227ebfa7a --- /dev/null +++ b/changelogs/unreleased/replace_spinach_spec_profile_notifications-feature.yml @@ -0,0 +1,4 @@ +--- +title: Replace 'profile/notifications.feature' spinach test with an rspec analog +merge_request: 12345 +author: @blackst0ne diff --git a/changelogs/unreleased/replase_spinach_spec_create-feature.yml b/changelogs/unreleased/replase_spinach_spec_create-feature.yml new file mode 100644 index 00000000000..0613d195d56 --- /dev/null +++ b/changelogs/unreleased/replase_spinach_spec_create-feature.yml @@ -0,0 +1,4 @@ +--- +title: Replace 'create.feature' spinach test with an rspec analog +merge_request: 12343 +author: @blackst0ne diff --git a/changelogs/unreleased/rework-authorizations-performance.yml b/changelogs/unreleased/rework-authorizations-performance.yml deleted file mode 100644 index f64257a6f56..00000000000 --- a/changelogs/unreleased/rework-authorizations-performance.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: > - Project authorizations are calculated much faster when using PostgreSQL, and - nested groups support for MySQL has been removed -merge_request: 10885 -author: diff --git a/changelogs/unreleased/search-restrict-projects-to-group.yml b/changelogs/unreleased/search-restrict-projects-to-group.yml deleted file mode 100644 index ac134bc5bce..00000000000 --- a/changelogs/unreleased/search-restrict-projects-to-group.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Restricts search projects dropdown to group projects when group is selected -merge_request: -author: diff --git a/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml b/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml deleted file mode 100644 index 1e783811b66..00000000000 --- a/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Properly handle container registry redirects to fix metadata stored on a S3 backend -merge_request: -author: diff --git a/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml b/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml deleted file mode 100644 index 161bce45601..00000000000 --- a/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix LFS timeouts when trying to save large files -merge_request: -author: diff --git a/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml b/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml deleted file mode 100644 index 255608bd89a..00000000000 --- a/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Set artifact working directory to be in the destination store to prevent unnecessary I/O -merge_request: -author: diff --git a/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml deleted file mode 100644 index d633995d467..00000000000 --- a/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Strip trailing whitespaces in submodule URLs -merge_request: -author: diff --git a/changelogs/unreleased/sh-recaptcha-fix-try2.yml b/changelogs/unreleased/sh-recaptcha-fix-try2.yml deleted file mode 100644 index 94729252c6f..00000000000 --- a/changelogs/unreleased/sh-recaptcha-fix-try2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make sure reCAPTCHA configuration is loaded when spam checks are initiated -merge_request: -author: diff --git a/changelogs/unreleased/speed-up-graphs.yml b/changelogs/unreleased/speed-up-graphs.yml new file mode 100644 index 00000000000..7cb155af6fd --- /dev/null +++ b/changelogs/unreleased/speed-up-graphs.yml @@ -0,0 +1,4 @@ +--- +title: Speed up used languages calculation on charts page +merge_request: +author: diff --git a/changelogs/unreleased/sync-email-from-omniauth.yml b/changelogs/unreleased/sync-email-from-omniauth.yml deleted file mode 100644 index ed14a95a5f1..00000000000 --- a/changelogs/unreleased/sync-email-from-omniauth.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sync email address from specified omniauth provider -merge_request: 11268 -author: Robin Bobbitt diff --git a/changelogs/unreleased/task-list-2.yml b/changelogs/unreleased/task-list-2.yml deleted file mode 100644 index cbae8178081..00000000000 --- a/changelogs/unreleased/task-list-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update task_list to version 2.0.0 -merge_request: 11525 -author: Jared Deckard <jared.deckard@gmail.com> diff --git a/changelogs/unreleased/tc-cache-trackable-attributes.yml b/changelogs/unreleased/tc-cache-trackable-attributes.yml deleted file mode 100644 index 4a2cf50893a..00000000000 --- a/changelogs/unreleased/tc-cache-trackable-attributes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Limit User's trackable attributes, like `current_sign_in_at`, to update at most once/hour" -merge_request: 11053 -author: diff --git a/changelogs/unreleased/tc-clean-pending-delete-projects.yml b/changelogs/unreleased/tc-clean-pending-delete-projects.yml deleted file mode 100644 index 31b43999c31..00000000000 --- a/changelogs/unreleased/tc-clean-pending-delete-projects.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add post-deploy migration to clean up projects in `pending_delete` state -merge_request: 11044 -author: diff --git a/changelogs/unreleased/tc-improve-project-api-perf.yml b/changelogs/unreleased/tc-improve-project-api-perf.yml deleted file mode 100644 index 7e88466c058..00000000000 --- a/changelogs/unreleased/tc-improve-project-api-perf.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve performance of ProjectFinder used in /projects API endpoint -merge_request: 11666 -author: diff --git a/changelogs/unreleased/tc-refactor-projects-finder-init-collection.yml b/changelogs/unreleased/tc-refactor-projects-finder-init-collection.yml new file mode 100644 index 00000000000..7bcbd6468c7 --- /dev/null +++ b/changelogs/unreleased/tc-refactor-projects-finder-init-collection.yml @@ -0,0 +1,4 @@ +--- +title: Add User#full_private_access? to check if user has access to all private groups & projects +merge_request: 12373 +author: diff --git a/changelogs/unreleased/up-arrow-focus-discussion-comment.yml b/changelogs/unreleased/up-arrow-focus-discussion-comment.yml deleted file mode 100644 index 5457dab6d3d..00000000000 --- a/changelogs/unreleased/up-arrow-focus-discussion-comment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix up arrow not editing last discussion comment -merge_request: -author: diff --git a/changelogs/unreleased/update-admin-health-page.yml b/changelogs/unreleased/update-admin-health-page.yml deleted file mode 100644 index 51aa6682b49..00000000000 --- a/changelogs/unreleased/update-admin-health-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added application readiness endpoints to the monitoring health check admin - view -merge_request: -author: diff --git a/changelogs/unreleased/update_bootsnap_1-1-1.yml b/changelogs/unreleased/update_bootsnap_1-1-1.yml new file mode 100644 index 00000000000..9ecfe4b60c8 --- /dev/null +++ b/changelogs/unreleased/update_bootsnap_1-1-1.yml @@ -0,0 +1,4 @@ +--- +title: Bump bootsnap to 1.1.1 +merge_request: 12425 +author: @blackst0ne diff --git a/changelogs/unreleased/use_relative_path_for_project_avatars.yml b/changelogs/unreleased/use_relative_path_for_project_avatars.yml deleted file mode 100644 index e3d0c0e1187..00000000000 --- a/changelogs/unreleased/use_relative_path_for_project_avatars.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use relative paths for group/project/user avatars -merge_request: 11001 -author: blackst0ne diff --git a/changelogs/unreleased/wait-for-ajax-handling-all-js-requests.yml b/changelogs/unreleased/wait-for-ajax-handling-all-js-requests.yml deleted file mode 100644 index 14aebe792c2..00000000000 --- a/changelogs/unreleased/wait-for-ajax-handling-all-js-requests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use wait_for_requests for both ajax and Vue requests -merge_request: -author: diff --git a/changelogs/unreleased/winh-current-user-filter.yml b/changelogs/unreleased/winh-current-user-filter.yml deleted file mode 100644 index e5409827b31..00000000000 --- a/changelogs/unreleased/winh-current-user-filter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show current user immediately in issuable filters -merge_request: 11630 -author: diff --git a/changelogs/unreleased/winh-pipeline-author-link.yml b/changelogs/unreleased/winh-pipeline-author-link.yml deleted file mode 100644 index 1b903d1e357..00000000000 --- a/changelogs/unreleased/winh-pipeline-author-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Link to commit author user page from pipelines -merge_request: 11100 -author: diff --git a/changelogs/unreleased/winh-styled-people-search-bar.yml b/changelogs/unreleased/winh-styled-people-search-bar.yml deleted file mode 100644 index a088af37d8d..00000000000 --- a/changelogs/unreleased/winh-styled-people-search-bar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Style people in issuable search bar -merge_request: 11402 -author: diff --git a/changelogs/unreleased/zj-clean-up-ci-variables-table.yml b/changelogs/unreleased/zj-clean-up-ci-variables-table.yml deleted file mode 100644 index ea2db40d590..00000000000 --- a/changelogs/unreleased/zj-clean-up-ci-variables-table.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Cleanup ci_variables schema and table -merge_request: -author: diff --git a/changelogs/unreleased/zj-drop-fk-if-exists.yml b/changelogs/unreleased/zj-drop-fk-if-exists.yml deleted file mode 100644 index 237ba936de9..00000000000 --- a/changelogs/unreleased/zj-drop-fk-if-exists.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove foreigh key on ci_trigger_schedules only if it exists -merge_request: -author: diff --git a/changelogs/unreleased/zj-faster-charts-page.yml b/changelogs/unreleased/zj-faster-charts-page.yml new file mode 100644 index 00000000000..9afcf111328 --- /dev/null +++ b/changelogs/unreleased/zj-faster-charts-page.yml @@ -0,0 +1,4 @@ +--- +title: Improve performance of the pipeline charts page +merge_request: 12378 +author: diff --git a/changelogs/unreleased/zj-i18n-pipeline-schedules.yml b/changelogs/unreleased/zj-i18n-pipeline-schedules.yml deleted file mode 100644 index 51c82a16359..00000000000 --- a/changelogs/unreleased/zj-i18n-pipeline-schedules.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow translation of Pipeline Schedules -merge_request: -author: diff --git a/changelogs/unreleased/zj-job-view-goes-real-time.yml b/changelogs/unreleased/zj-job-view-goes-real-time.yml deleted file mode 100644 index 376c9dfa65f..00000000000 --- a/changelogs/unreleased/zj-job-view-goes-real-time.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Job details page update real time -merge_request: 11651 -author: diff --git a/changelogs/unreleased/zj-pipeline-schedule-owner.yml b/changelogs/unreleased/zj-pipeline-schedule-owner.yml deleted file mode 100644 index be704e173ab..00000000000 --- a/changelogs/unreleased/zj-pipeline-schedule-owner.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add foreign key for pipeline schedule owner -merge_request: 11233 -author: diff --git a/changelogs/unreleased/zj-prom-pipeline-count.yml b/changelogs/unreleased/zj-prom-pipeline-count.yml deleted file mode 100644 index 191e4f2f949..00000000000 --- a/changelogs/unreleased/zj-prom-pipeline-count.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add prometheus metrics on pipeline creation -merge_request: -author: diff --git a/changelogs/unreleased/zj-read-registry-pat.yml b/changelogs/unreleased/zj-read-registry-pat.yml deleted file mode 100644 index d36159bbdf5..00000000000 --- a/changelogs/unreleased/zj-read-registry-pat.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow pulling of container images using personal access tokens -merge_request: 11845 -author: diff --git a/changelogs/unreleased/zj-realtime-env-list.yml b/changelogs/unreleased/zj-realtime-env-list.yml deleted file mode 100644 index 6460d17edc9..00000000000 --- a/changelogs/unreleased/zj-realtime-env-list.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make environment table realtime -merge_request: 11333 -author: diff --git a/changelogs/unreleased/zj-review-apps-usage-data.yml b/changelogs/unreleased/zj-review-apps-usage-data.yml new file mode 100644 index 00000000000..7d224d0fc32 --- /dev/null +++ b/changelogs/unreleased/zj-review-apps-usage-data.yml @@ -0,0 +1,4 @@ +--- +title: Add review apps to usage metrics +merge_request: 12185 +author: diff --git a/changelogs/unreleased/zj-sort-env-folders.yml b/changelogs/unreleased/zj-sort-env-folders.yml deleted file mode 100644 index b3ca97aef94..00000000000 --- a/changelogs/unreleased/zj-sort-env-folders.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sort folder for environments -merge_request: -author: diff --git a/config.ru b/config.ru index 89aba462f19..065ce59932f 100644 --- a/config.ru +++ b/config.ru @@ -15,9 +15,6 @@ if defined?(Unicorn) end end -# set default directory for multiproces metrics gathering -ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' - require ::File.expand_path('../config/environment', __FILE__) map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do diff --git a/config/application.rb b/config/application.rb index 8bbecf3ed0f..3f39170a123 100644 --- a/config/application.rb +++ b/config/application.rb @@ -109,6 +109,8 @@ module Gitlab config.assets.precompile << "lib/ace.js" config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "test.css" + config.assets.precompile << "new_nav.css" + config.assets.precompile << "new_sidebar.css" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/boot.rb b/config/boot.rb index 17a71148370..02baeab29ab 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -5,14 +5,13 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -# Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage -require 'bootsnap' -Bootsnap.setup( - cache_dir: 'tmp/cache', - development_mode: ENV['RAILS_ENV'] == 'development', - load_path_cache: true, - autoload_paths_cache: true, - disable_trace: false, - compile_cache_iseq: true, - compile_cache_yaml: true -) +begin + require 'bootsnap/setup' +rescue SystemCallError => exception + $stderr.puts "WARNING: Bootsnap failed to setup: #{exception.message}" +end + +# set default directory for multiproces metrics gathering +if ENV['RAILS_ENV'] == 'development' || ENV['RAILS_ENV'] == 'test' + ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' +end diff --git a/config/environments/production.rb b/config/environments/production.rb index 82a19085b1d..c5cbfcf64cf 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -50,7 +50,7 @@ Rails.application.configure do # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Enable serving of images, stylesheets, and JavaScripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" + config.action_controller.asset_host = ENV['GITLAB_CDN_HOST'] if ENV['GITLAB_CDN_HOST'].present? # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 0b33783869b..43a8c0078ca 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -454,6 +454,10 @@ production: &base # introduced in 9.0). Eventually Gitaly use will become mandatory and # this option will disappear. enabled: true + # Default Gitaly authentication token. Can be overriden per storage. Can + # be left blank when Gitaly is running locally on a Unix socket, which + # is the normal way to deploy Gitaly. + token: # # 4. Advanced settings @@ -469,6 +473,7 @@ production: &base default: path: /home/git/repositories/ gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port) + # gitaly_token: 'special token' # Optional: override global gitaly.token for this storage. ## Backup settings backup: @@ -594,6 +599,7 @@ test: gitaly_address: unix:tmp/tests/gitaly/gitaly.socket gitaly: enabled: true + token: secret backup: path: tmp/tests/backups gitlab_shell: diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index 5e0eefdb154..a0a63ddf8f0 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -113,6 +113,9 @@ def instrument_classes(instrumentation) # This is a Rails scope so we have to instrument it manually. instrumentation.instrument_method(Project, :visible_to_user) + + # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159 + instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits) end # rubocop:enable Metrics/AbcSize @@ -151,8 +154,8 @@ if Gitlab::Metrics.enabled? ActiveRecord::Querying.public_instance_methods(false).map(&:to_s) ) - Gitlab::Metrics::Instrumentation. - instrument_class_hierarchy(ActiveRecord::Base) do |klass, method| + Gitlab::Metrics::Instrumentation + .instrument_class_hierarchy(ActiveRecord::Base) do |klass, method| # Instrumenting the ApplicationSetting class can lead to an infinite # loop. Since the data is cached any way we don't really need to # instrument it. diff --git a/config/initializers/active_record_data_types.rb b/config/initializers/active_record_data_types.rb index beb97c6fce0..fef591c397d 100644 --- a/config/initializers/active_record_data_types.rb +++ b/config/initializers/active_record_data_types.rb @@ -4,21 +4,78 @@ if Gitlab::Database.postgresql? require 'active_record/connection_adapters/postgresql_adapter' - module ActiveRecord - module ConnectionAdapters - class PostgreSQLAdapter - NATIVE_DATABASE_TYPES.merge!(datetime_with_timezone: { name: 'timestamptz' }) + module ActiveRecord::ConnectionAdapters::PostgreSQL::OID + # Add the class `DateTimeWithTimeZone` so we can map `timestamptz` to it. + class DateTimeWithTimeZone < DateTime + def type + :datetime_with_timezone end end end + + module RegisterDateTimeWithTimeZone + # Run original `initialize_type_map` and then register `timestamptz` as a + # `DateTimeWithTimeZone`. + # + # Apparently it does not matter that the original `initialize_type_map` + # aliases `timestamptz` to `timestamp`. + # + # When schema dumping, `timestamptz` columns will be output as + # `t.datetime_with_timezone`. + def initialize_type_map(mapping) + super mapping + + mapping.register_type 'timestamptz' do |_, _, sql_type| + precision = extract_precision(sql_type) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::DateTimeWithTimeZone.new(precision: precision) + end + end + end + + class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter + prepend RegisterDateTimeWithTimeZone + + # Add column type `datetime_with_timezone` so we can do this in + # migrations: + # + # add_column(:users, :datetime_with_timezone) + # + NATIVE_DATABASE_TYPES[:datetime_with_timezone] = { name: 'timestamptz' } + end elsif Gitlab::Database.mysql? require 'active_record/connection_adapters/mysql2_adapter' - module ActiveRecord - module ConnectionAdapters - class AbstractMysqlAdapter - NATIVE_DATABASE_TYPES.merge!(datetime_with_timezone: { name: 'timestamp' }) + module RegisterDateTimeWithTimeZone + # Run original `initialize_type_map` and then register `timestamp` as a + # `MysqlDateTimeWithTimeZone`. + # + # When schema dumping, `timestamp` columns will be output as + # `t.datetime_with_timezone`. + def initialize_type_map(mapping) + super mapping + + mapping.register_type(%r(timestamp)i) do |sql_type| + precision = extract_precision(sql_type) + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlDateTimeWithTimeZone.new(precision: precision) end end end + + class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter + prepend RegisterDateTimeWithTimeZone + + # Add the class `DateTimeWithTimeZone` so we can map `timestamp` to it. + class MysqlDateTimeWithTimeZone < MysqlDateTime + def type + :datetime_with_timezone + end + end + + # Add column type `datetime_with_timezone` so we can do this in + # migrations: + # + # add_column(:users, :datetime_with_timezone) + # + NATIVE_DATABASE_TYPES[:datetime_with_timezone] = { name: 'timestamp' } + end end diff --git a/config/initializers/active_record_table_definition.rb b/config/initializers/active_record_table_definition.rb index 4f59e35f4da..8e3a1c7a62f 100644 --- a/config/initializers/active_record_table_definition.rb +++ b/config/initializers/active_record_table_definition.rb @@ -3,15 +3,15 @@ require 'active_record/connection_adapters/abstract/schema_definitions' -# Appends columns `created_at` and `updated_at` to a table. -# -# It is used in table creation like: -# create_table 'users' do |t| -# t.timestamps_with_timezone -# end module ActiveRecord module ConnectionAdapters class TableDefinition + # Appends columns `created_at` and `updated_at` to a table. + # + # It is used in table creation like: + # create_table 'users' do |t| + # t.timestamps_with_timezone + # end def timestamps_with_timezone(**options) options[:null] = false if options[:null].nil? @@ -19,6 +19,16 @@ module ActiveRecord column(column_name, :datetime_with_timezone, options) end end + + # Adds specified column with appropriate timestamp type + # + # It is used in table creation like: + # create_table 'users' do |t| + # t.datetime_with_timezone :did_something_at + # end + def datetime_with_timezone(column_name, **options) + column(column_name, :datetime_with_timezone, options) + end end end end diff --git a/config/initializers/bootstrap_form.rb b/config/initializers/bootstrap_form.rb new file mode 100644 index 00000000000..11171b38a85 --- /dev/null +++ b/config/initializers/bootstrap_form.rb @@ -0,0 +1,7 @@ +module BootstrapFormBuilderCustomization + def label_class + "label-light" + end +end + +BootstrapForm::FormBuilder.prepend(BootstrapFormBuilderCustomization) diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb new file mode 100644 index 00000000000..0fee832788d --- /dev/null +++ b/config/initializers/flipper.rb @@ -0,0 +1,4 @@ +require 'flipper/middleware/memoizer' + +Rails.application.config.middleware.use Flipper::Middleware::Memoizer, + lambda { Feature.flipper } diff --git a/config/initializers/rugged_use_gitlab_git_attributes.rb b/config/initializers/rugged_use_gitlab_git_attributes.rb new file mode 100644 index 00000000000..7d652799786 --- /dev/null +++ b/config/initializers/rugged_use_gitlab_git_attributes.rb @@ -0,0 +1,25 @@ +# We don't want to ever call Rugged::Repository#fetch_attributes, because it has +# a lot of I/O overhead: +# <https://gitlab.com/gitlab-org/gitlab_git/commit/340e111e040ae847b614d35b4d3173ec48329015> +# +# While we don't do this from within the GitLab source itself, the Linguist gem +# has a dependency on Rugged and uses the gitattributes file when calculating +# repository-wide language statistics: +# <https://github.com/github/linguist/blob/v4.7.0/lib/linguist/lazy_blob.rb#L33-L36> +# +# The options passed by Linguist are those assumed by Gitlab::Git::Attributes +# anyway, and there is no great efficiency gain from just fetching the listed +# attributes with our implementation, so we ignore the additional arguments. +# +module Rugged + class Repository + module UseGitlabGitAttributes + def fetch_attributes(name, *) + @attributes ||= Gitlab::Git::Attributes.new(path) + @attributes.attributes(name) + end + end + + prepend UseGitlabGitAttributes + end +end diff --git a/config/karma.config.js b/config/karma.config.js index 40c58e7771d..2f571978e08 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -21,7 +21,18 @@ module.exports = function(config) { var karmaConfig = { basePath: ROOT_PATH, - browsers: ['PhantomJS'], + browsers: ['ChromeHeadlessCustom'], + customLaunchers: { + ChromeHeadlessCustom: { + base: 'ChromeHeadless', + displayName: 'Chrome', + flags: [ + // chrome cannot run in sandboxed mode inside a docker container unless it is run with + // escalated kernel privileges (e.g. docker run --cap-add=CAP_SYS_ADMIN) + '--no-sandbox', + ], + } + }, frameworks: ['jasmine'], files: [ { pattern: 'spec/javascripts/test_bundle.js', watched: false }, @@ -43,6 +54,25 @@ module.exports = function(config) { subdir: '.', fixWebpackSourcePaths: true }; + karmaConfig.browserNoActivityTimeout = 60000; // 60 seconds + } + + if (process.env.DEBUG) { + karmaConfig.logLevel = config.LOG_DEBUG; + process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log'; + } + + if (process.env.CHROME_LOG_FILE) { + karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); + } + + if (process.env.DEBUG) { + karmaConfig.logLevel = config.LOG_DEBUG; + process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log'; + } + + if (process.env.CHROME_LOG_FILE) { + karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); } config.set(karmaConfig); diff --git a/config/locales/en.yml b/config/locales/en.yml index 9d47425950a..8932db138d9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,17 +2,63 @@ # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: - hello: "Hello world" - errors: - messages: - label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one." - wrong_size: "is the wrong size (should be %{file_size})" - size_too_small: "is too small (should be at least %{file_size})" - size_too_big: "is too big (should be at most %{file_size})" views: pagination: previous: "Prev" next: "Next" + date: + abbr_day_names: + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat + abbr_month_names: + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec + day_names: + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday + formats: + default: "%Y-%m-%d" + long: "%B %d, %Y" + short: "%b %d" + month_names: + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December + order: + - :year + - :month + - :day datetime: time_ago_in_words: half_a_minute: "half a minute ago" @@ -49,3 +95,158 @@ en: almost_x_years: one: "almost 1 year ago" other: "almost %{count} years ago" + distance_in_words: + about_x_hours: + one: about 1 hour + other: about %{count} hours + about_x_months: + one: about 1 month + other: about %{count} months + about_x_years: + one: about 1 year + other: about %{count} years + almost_x_years: + one: almost 1 year + other: almost %{count} years + half_a_minute: half a minute + less_than_x_minutes: + one: less than a minute + other: less than %{count} minutes + less_than_x_seconds: + one: less than 1 second + other: less than %{count} seconds + over_x_years: + one: over 1 year + other: over %{count} years + x_days: + one: 1 day + other: "%{count} days" + x_minutes: + one: 1 minute + other: "%{count} minutes" + x_months: + one: 1 month + other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" + x_seconds: + one: 1 second + other: "%{count} seconds" + prompts: + day: Day + hour: Hour + minute: Minute + month: Month + second: Seconds + year: Year + errors: + format: "%{attribute} %{message}" + messages: + label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one." + wrong_size: "is the wrong size (should be %{file_size})" + size_too_small: "is too small (should be at least %{file_size})" + size_too_big: "is too big (should be at most %{file_size})" + accepted: must be accepted + blank: can't be blank + present: must be blank + confirmation: doesn't match %{attribute} + empty: can't be empty + equal_to: must be equal to %{count} + even: must be even + exclusion: is reserved + greater_than: must be greater than %{count} + greater_than_or_equal_to: must be greater than or equal to %{count} + inclusion: is not included in the list + invalid: is invalid + less_than: must be less than %{count} + less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" + not_a_number: is not a number + not_an_integer: must be an integer + odd: must be odd + required: must exist + taken: has already been taken + too_long: + one: is too long (maximum is 1 character) + other: is too long (maximum is %{count} characters) + too_short: + one: is too short (minimum is 1 character) + other: is too short (minimum is %{count} characters) + wrong_length: + one: is the wrong length (should be 1 character) + other: is the wrong length (should be %{count} characters) + other_than: must be other than %{count} + template: + body: 'There were problems with the following fields:' + header: + one: 1 error prohibited this %{model} from being saved + other: "%{count} errors prohibited this %{model} from being saved" + helpers: + select: + prompt: Please select + submit: + create: Create %{model} + submit: Save %{model} + update: Update %{model} + number: + currency: + format: + delimiter: "," + format: "%u%n" + precision: 2 + separator: "." + significant: false + strip_insignificant_zeros: false + unit: "$" + format: + delimiter: "," + precision: 3 + separator: "." + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: Billion + million: Million + quadrillion: Quadrillion + thousand: Thousand + trillion: Trillion + unit: '' + format: + delimiter: '' + precision: 3 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: Byte + other: Bytes + gb: GB + kb: KB + mb: MB + tb: TB + percentage: + format: + delimiter: '' + format: "%n%" + precision: + format: + delimiter: '' + support: + array: + last_word_connector: ", and " + two_words_connector: " and " + words_connector: ", " + time: + am: am + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + long: "%B %d, %Y %H:%M" + short: "%d %b %H:%M" + timeago_tooltip: "%b %-d, %Y %-l:%M%P" + pm: pm diff --git a/config/locales/es.yml b/config/locales/es.yml index d71c6eb5047..fdc52b4ae11 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -251,4 +251,5 @@ es: default: "%A, %d de %B de %Y %H:%M:%S %z" long: "%d de %B de %Y %H:%M" short: "%d de %b %H:%M" + timeago_tooltip: "%d de %B de %Y %H:%M" pm: pm diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml new file mode 100644 index 00000000000..daecde49570 --- /dev/null +++ b/config/prometheus/additional_metrics.yml @@ -0,0 +1,32 @@ +- group: Kubernetes + priority: 1 + metrics: + - title: "Memory usage" + y_label: "Values" + required_metrics: + - container_memory_usage_bytes + weight: 1 + queries: + - query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20' + label: Container memory + unit: MiB + - title: "Current memory usage" + required_metrics: + - container_memory_usage_bytes + weight: 1 + queries: + - query: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20' + display_empty: false + unit: MiB + - title: "CPU usage" + required_metrics: + - container_cpu_usage_seconds_total + weight: 1 + queries: + - query_range: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100' + - title: "Current CPU usage" + required_metrics: + - container_cpu_usage_seconds_total + weight: 1 + queries: + - query: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100' diff --git a/config/routes/project.rb b/config/routes/project.rb index f95cc3101d3..19e18c733b1 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -73,6 +73,10 @@ constraints(ProjectUrlConstrainer.new) do resource :mattermost, only: [:new, :create] + namespace :prometheus do + get :active_metrics + end + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create, :edit, :update] do member do put :enable @@ -153,6 +157,7 @@ constraints(ProjectUrlConstrainer.new) do post :stop get :terminal get :metrics + get :additional_metrics get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } end @@ -163,6 +168,7 @@ constraints(ProjectUrlConstrainer.new) do resources :deployments, only: [:index] do member do get :metrics + get :additional_metrics end end end diff --git a/config/webpack.config.js b/config/webpack.config.js index 120f9d3193d..2e8c94655c1 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -11,22 +11,13 @@ var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeMod var ROOT_PATH = path.resolve(__dirname, '..'); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; -var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; +var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var NO_COMPRESSION = process.env.NO_COMPRESSION; -// optional dependency `node-zopfli` is unavailable on CentOS 6 -var ZOPFLI_AVAILABLE; -try { - require.resolve('node-zopfli'); - ZOPFLI_AVAILABLE = true; -} catch(err) { - ZOPFLI_AVAILABLE = false; -} - var config = { // because sqljs requires fs. node: { @@ -61,9 +52,10 @@ var config = { network: './network/network_bundle.js', notebook_viewer: './blob/notebook_viewer.js', pdf_viewer: './blob/pdf_viewer.js', - pipelines: './pipelines/index.js', + pipelines: './pipelines/pipelines_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js', profile: './profile/profile_bundle.js', + prometheus_metrics: './prometheus_metrics', protected_branches: './protected_branches/protected_branches_bundle.js', protected_tags: './protected_tags', sidebar: './sidebar/sidebar_bundle.js', @@ -233,12 +225,12 @@ if (IS_PRODUCTION) { // zopfli requires a lot of compute time and is disabled in CI if (!NO_COMPRESSION) { - config.plugins.push( - new CompressionPlugin({ - asset: '[path].gz[query]', - algorithm: ZOPFLI_AVAILABLE ? 'zopfli' : 'gzip', - }) - ); + // gracefully fall back to gzip if `node-zopfli` is unavailable (e.g. in CentOS 6) + try { + config.plugins.push(new CompressionPlugin({ algorithm: 'zopfli' })); + } catch(err) { + config.plugins.push(new CompressionPlugin({ algorithm: 'gzip' })); + } } } @@ -249,6 +241,7 @@ if (IS_DEV_SERVER) { port: DEV_SERVER_PORT, headers: { 'Access-Control-Allow-Origin': '*' }, stats: 'errors-only', + hot: DEV_SERVER_LIVERELOAD, inline: DEV_SERVER_LIVERELOAD }; config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath; @@ -256,6 +249,9 @@ if (IS_DEV_SERVER) { // watch node_modules for changes if we encounter a missing module compile error new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) ); + if (DEV_SERVER_LIVERELOAD) { + config.plugins.push(new webpack.HotModuleReplacementPlugin()); + } } if (WEBPACK_REPORT) { diff --git a/db/fixtures/development/19_environments.rb b/db/fixtures/development/19_environments.rb index 93214b9d3e7..c1bbc9af6d6 100644 --- a/db/fixtures/development/19_environments.rb +++ b/db/fixtures/development/19_environments.rb @@ -33,7 +33,7 @@ class Gitlab::Seeder::Environments create_deployment!( merge_request.source_project, - "review/#{merge_request.source_branch}", + "review/#{merge_request.source_branch.gsub(/[^a-zA-Z0-9]/, '')}", merge_request.source_branch, merge_request.diff_head_sha ) diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb index 4d6a61bd614..5336b036bca 100644 --- a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb +++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb @@ -2,6 +2,8 @@ class SetMissingStageOnCiBuilds < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + def up update_column_in_batches(:ci_builds, :stage, :test) do |table, query| query.where(table[:stage].eq(nil)) diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb index b2a2ce41391..abe8e701e23 100644 --- a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb +++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb @@ -5,6 +5,8 @@ class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false + disable_ddl_transaction! + def up update_column_in_batches(:projects, :has_external_wiki, nil) do |table, query| query.where(table[:has_external_wiki].not_eq(nil)) diff --git a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb index febd2c0e65e..f8486e3e1a6 100644 --- a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb +++ b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb @@ -4,6 +4,8 @@ class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration DOWNTIME = false + disable_ddl_transaction! + def up update_column_in_batches(:web_hooks, :confidential_issues_events, true) do |table, query| query.where(table[:issues_events].eq(true)) diff --git a/db/migrate/20160919144305_add_type_to_labels.rb b/db/migrate/20160919144305_add_type_to_labels.rb index 2d2725ccf59..d08b339cd27 100644 --- a/db/migrate/20160919144305_add_type_to_labels.rb +++ b/db/migrate/20160919144305_add_type_to_labels.rb @@ -5,6 +5,8 @@ class AddTypeToLabels < ActiveRecord::Migration DOWNTIME = true DOWNTIME_REASON = 'Labels will not work as expected until this migration is complete.' + disable_ddl_transaction! + def change add_column :labels, :type, :string diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb index fe11699c196..cb93b449067 100644 --- a/db/migrate/20161018124658_make_project_owners_masters.rb +++ b/db/migrate/20161018124658_make_project_owners_masters.rb @@ -4,6 +4,8 @@ class MakeProjectOwnersMasters < ActiveRecord::Migration DOWNTIME = false + disable_ddl_transaction! + def up update_column_in_batches(:members, :access_level, 40) do |table, query| query.where(table[:access_level].eq(50).and(table[:source_type].eq('Project'))) diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb index e5292cfba07..c0cb9d78748 100644 --- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb +++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb @@ -6,9 +6,9 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration class Project < ActiveRecord::Base def self.find_including_path(id) - select("projects.*, CONCAT(namespaces.path, '/', projects.path) AS path_with_namespace"). - joins('INNER JOIN namespaces ON namespaces.id = projects.namespace_id'). - find_by(id: id) + select("projects.*, CONCAT(namespaces.path, '/', projects.path) AS path_with_namespace") + .joins('INNER JOIN namespaces ON namespaces.id = projects.namespace_id') + .find_by(id: id) end def repository_storage_path diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb index a20a903a752..f73e4f6c99b 100644 --- a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb +++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb @@ -8,11 +8,11 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration environments = Arel::Table.new(:environments) # Get all [project_id, name] pairs that occur more than once - finder_sql = environments. - group(environments[:project_id], environments[:name]). - having(Arel.sql("COUNT(1)").gt(1)). - project(environments[:project_id], environments[:name]). - to_sql + finder_sql = environments + .group(environments[:project_id], environments[:name]) + .having(Arel.sql("COUNT(1)").gt(1)) + .project(environments[:project_id], environments[:name]) + .to_sql conflicting = connection.exec_query(finder_sql) @@ -28,12 +28,12 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration # Rename conflicting environments by appending "-#{id}" to all but the first def fix_duplicates(project_id, name) environments = Arel::Table.new(:environments) - finder_sql = environments. - where(environments[:project_id].eq(project_id)). - where(environments[:name].eq(name)). - order(environments[:id].asc). - project(environments[:id], environments[:name]). - to_sql + finder_sql = environments + .where(environments[:project_id].eq(project_id)) + .where(environments[:name].eq(name)) + .order(environments[:id].asc) + .project(environments[:id], environments[:name]) + .to_sql # Now we have the data for all the conflicting rows conflicts = connection.exec_query(finder_sql).rows @@ -41,11 +41,11 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration conflicts.each do |id, name| update_sql = - Arel::UpdateManager.new(ActiveRecord::Base). - table(environments). - set(environments[:name] => name + "-" + id.to_s). - where(environments[:id].eq(id)). - to_sql + Arel::UpdateManager.new(ActiveRecord::Base) + .table(environments) + .set(environments[:name] => name + "-" + id.to_s) + .where(environments[:id].eq(id)) + .to_sql connection.exec_update(update_sql, self.class.name, []) end diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb index 8e98ee5b9ba..83cdd484c4c 100644 --- a/db/migrate/20161207231626_add_environment_slug.rb +++ b/db/migrate/20161207231626_add_environment_slug.rb @@ -19,10 +19,10 @@ class AddEnvironmentSlug < ActiveRecord::Migration finder = environments.project(:id, :name) connection.exec_query(finder.to_sql).rows.each do |id, name| - updater = Arel::UpdateManager.new(ActiveRecord::Base). - table(environments). - set(environments[:slug] => generate_slug(name)). - where(environments[:id].eq(id)) + updater = Arel::UpdateManager.new(ActiveRecord::Base) + .table(environments) + .set(environments[:slug] => generate_slug(name)) + .where(environments[:id].eq(id)) connection.exec_update(updater.to_sql, self.class.name, []) end diff --git a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb index c7cada6dfc5..6b15e5caccf 100644 --- a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb +++ b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb @@ -4,6 +4,8 @@ class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration DOWNTIME = false + disable_ddl_transaction! + def up update_column_in_batches(:services, :type, 'SlackService') do |table, query| query.where(table[:type].eq('SlackNotificationService')) diff --git a/db/migrate/20170316163800_rename_system_namespaces.rb b/db/migrate/20170316163800_rename_system_namespaces.rb index b5408fbf112..9e9fb5ac225 100644 --- a/db/migrate/20170316163800_rename_system_namespaces.rb +++ b/db/migrate/20170316163800_rename_system_namespaces.rb @@ -159,9 +159,9 @@ class RenameSystemNamespaces < ActiveRecord::Migration end def system_namespace - @system_namespace ||= Namespace.where(parent_id: nil). - where(arel_table[:path].matches(system_namespace_path)). - first + @system_namespace ||= Namespace.where(parent_id: nil) + .where(arel_table[:path].matches(system_namespace_path)) + .first end def system_namespace_path @@ -209,8 +209,8 @@ class RenameSystemNamespaces < ActiveRecord::Migration end def repo_paths_for_namespace(namespace) - projects_for_namespace(namespace).distinct. - select(:repository_storage).map(&:repository_storage_path) + projects_for_namespace(namespace).distinct + .select(:repository_storage).map(&:repository_storage_path) end def uploads_dir diff --git a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb index c67690642c9..33908ae1156 100644 --- a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb +++ b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb @@ -87,8 +87,8 @@ class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration while current&.parent_id # We're using find_by(id: ...) here to deal with cases where the # parent_id may point to a missing row. - current = Namespace.unscoped.select([:id, :parent_id]). - find_by(id: current.parent_id) + current = Namespace.unscoped.select([:id, :parent_id]) + .find_by(id: current.parent_id) ancestors << current.id if current end @@ -99,11 +99,11 @@ class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration # Returns a relation containing all the members that have access to any of # the current namespace's parent namespaces. def all_members_for(namespace) - Member. - unscoped. - select(['user_id', 'MAX(access_level) AS access_level']). - where(source_type: 'Namespace', source_id: ancestors_for(namespace)). - group(:user_id) + Member + .unscoped + .select(['user_id', 'MAX(access_level) AS access_level']) + .where(source_type: 'Namespace', source_id: ancestors_for(namespace)) + .group(:user_id) end def bulk_insert_members(rows) diff --git a/db/migrate/20170526185602_add_stage_id_to_ci_builds.rb b/db/migrate/20170526185602_add_stage_id_to_ci_builds.rb index d5675d5828b..d27cba76d81 100644 --- a/db/migrate/20170526185602_add_stage_id_to_ci_builds.rb +++ b/db/migrate/20170526185602_add_stage_id_to_ci_builds.rb @@ -3,19 +3,11 @@ class AddStageIdToCiBuilds < ActiveRecord::Migration DOWNTIME = false - disable_ddl_transaction! - def up add_column :ci_builds, :stage_id, :integer - - add_concurrent_foreign_key :ci_builds, :ci_stages, column: :stage_id, on_delete: :cascade - add_concurrent_index :ci_builds, :stage_id end def down - remove_foreign_key :ci_builds, column: :stage_id - remove_concurrent_index :ci_builds, :stage_id - remove_column :ci_builds, :stage_id, :integer end end diff --git a/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb new file mode 100644 index 00000000000..5e8b667b86d --- /dev/null +++ b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb @@ -0,0 +1,9 @@ +class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :help_page_hide_commercial_content, :boolean, default: false + end +end diff --git a/db/migrate/20170602154813_add_help_page_support_url_to_application_settings.rb b/db/migrate/20170602154813_add_help_page_support_url_to_application_settings.rb new file mode 100644 index 00000000000..138fe9b2a37 --- /dev/null +++ b/db/migrate/20170602154813_add_help_page_support_url_to_application_settings.rb @@ -0,0 +1,9 @@ +class AddHelpPageSupportUrlToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :help_page_support_url, :string + end +end diff --git a/db/migrate/20170606154216_add_notification_setting_columns.rb b/db/migrate/20170606154216_add_notification_setting_columns.rb new file mode 100644 index 00000000000..0a9b5da6583 --- /dev/null +++ b/db/migrate/20170606154216_add_notification_setting_columns.rb @@ -0,0 +1,26 @@ +class AddNotificationSettingColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + COLUMNS = [ + :new_note, + :new_issue, + :reopen_issue, + :close_issue, + :reassign_issue, + :new_merge_request, + :reopen_merge_request, + :close_merge_request, + :reassign_merge_request, + :merge_merge_request, + :failed_pipeline, + :success_pipeline + ] + + def change + COLUMNS.each do |column| + add_column(:notification_settings, column, :boolean) + end + end +end diff --git a/db/migrate/20170608171156_create_merge_request_diff_files.rb b/db/migrate/20170608171156_create_merge_request_diff_files.rb new file mode 100644 index 00000000000..bf0c0d29adc --- /dev/null +++ b/db/migrate/20170608171156_create_merge_request_diff_files.rb @@ -0,0 +1,22 @@ +class CreateMergeRequestDiffFiles < ActiveRecord::Migration + DOWNTIME = false + + disable_ddl_transaction! + + def change + create_table :merge_request_diff_files, id: false do |t| + t.belongs_to :merge_request_diff, null: false, foreign_key: { on_delete: :cascade } + t.integer :relative_order, null: false + t.boolean :new_file, null: false + t.boolean :renamed_file, null: false + t.boolean :deleted_file, null: false + t.boolean :too_large, null: false + t.string :a_mode, null: false + t.string :b_mode, null: false + t.text :new_path, null: false + t.text :old_path, null: false + t.text :diff, null: false + t.index [:merge_request_diff_id, :relative_order], name: 'index_merge_request_diff_files_on_mr_diff_id_and_order', unique: true + end + end +end diff --git a/db/migrate/20170614115405_merge_request_diff_file_limits_to_mysql.rb b/db/migrate/20170614115405_merge_request_diff_file_limits_to_mysql.rb new file mode 100644 index 00000000000..4c1cf08aa06 --- /dev/null +++ b/db/migrate/20170614115405_merge_request_diff_file_limits_to_mysql.rb @@ -0,0 +1 @@ +require_relative 'merge_request_diff_file_limits_to_mysql' diff --git a/db/migrate/20170619144837_add_index_for_head_pipeline_merge_request.rb b/db/migrate/20170619144837_add_index_for_head_pipeline_merge_request.rb new file mode 100644 index 00000000000..02863bee082 --- /dev/null +++ b/db/migrate/20170619144837_add_index_for_head_pipeline_merge_request.rb @@ -0,0 +1,15 @@ +class AddIndexForHeadPipelineMergeRequest < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :merge_requests, :head_pipeline_id + end + + def down + remove_concurrent_index :merge_requests, :head_pipeline_id if index_exists?(:merge_requests, :head_pipeline_id) + end +end diff --git a/db/migrate/20170622162730_add_ref_fetched_to_merge_request.rb b/db/migrate/20170622162730_add_ref_fetched_to_merge_request.rb new file mode 100644 index 00000000000..62aa1a4b4f0 --- /dev/null +++ b/db/migrate/20170622162730_add_ref_fetched_to_merge_request.rb @@ -0,0 +1,9 @@ +class AddRefFetchedToMergeRequest < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :merge_requests, :ref_fetched, :boolean + end +end diff --git a/db/migrate/merge_request_diff_file_limits_to_mysql.rb b/db/migrate/merge_request_diff_file_limits_to_mysql.rb new file mode 100644 index 00000000000..3958380e4b9 --- /dev/null +++ b/db/migrate/merge_request_diff_file_limits_to_mysql.rb @@ -0,0 +1,12 @@ +class MergeRequestDiffFileLimitsToMysql < ActiveRecord::Migration + DOWNTIME = false + + def up + return unless Gitlab::Database.mysql? + + change_column :merge_request_diff_files, :diff, :text, limit: 2147483647 + end + + def down + end +end diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb index 14b5ef476f0..69007b8e8ed 100644 --- a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb +++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb @@ -13,13 +13,13 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration namespaces = Arel::Table.new(:namespaces) finder_sql = - projects. - join(namespaces, Arel::Nodes::InnerJoin). - on(projects[:namespace_id].eq(namespaces[:id])). - where(projects[:visibility_level].gt(namespaces[:visibility_level])). - project(projects[:id], namespaces[:visibility_level]). - take(BATCH_SIZE). - to_sql + projects + .join(namespaces, Arel::Nodes::InnerJoin) + .on(projects[:namespace_id].eq(namespaces[:id])) + .where(projects[:visibility_level].gt(namespaces[:visibility_level])) + .project(projects[:id], namespaces[:visibility_level]) + .take(BATCH_SIZE) + .to_sql # Update matching rows in batches. Each batch can cause up to 3 UPDATE # statements, in addition to the SELECT: one per visibility_level @@ -33,10 +33,10 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration end updates.each do |visibility_level, project_ids| - updater = Arel::UpdateManager.new(ActiveRecord::Base). - table(projects). - set(projects[:visibility_level] => visibility_level). - where(projects[:id].in(project_ids)) + updater = Arel::UpdateManager.new(ActiveRecord::Base) + .table(projects) + .set(projects[:visibility_level] => visibility_level) + .where(projects[:id].in(project_ids)) ActiveRecord::Base.connection.exec_update(updater.to_sql, self.class.name, []) end diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb index 49a6bc884a8..d322844e2fd 100644 --- a/db/post_migrate/20161221153951_rename_reserved_project_names.rb +++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb @@ -79,17 +79,17 @@ class RenameReservedProjectNames < ActiveRecord::Migration private def reserved_projects - Project.unscoped. - includes(:namespace). - where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)'). - where('projects.path' => KNOWN_PATHS) + Project.unscoped + .includes(:namespace) + .where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)') + .where('projects.path' => KNOWN_PATHS) end def route_exists?(full_path) quoted_path = ActiveRecord::Base.connection.quote_string(full_path) - ActiveRecord::Base.connection. - select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present? + ActiveRecord::Base.connection + .select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present? end # Adds number to the end of the path that is not taken by other route diff --git a/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb index f399950bd5e..d7be004d47f 100644 --- a/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb +++ b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb @@ -39,11 +39,11 @@ class RequeuePendingDeleteProjects < ActiveRecord::Migration def find_batch projects = Arel::Table.new(:projects) - projects.project(projects[:id]). - where(projects[:pending_delete].eq(true)). - where(projects[:namespace_id].not_eq(nil)). - skip(@offset * BATCH_SIZE). - take(BATCH_SIZE). - to_sql + projects.project(projects[:id]) + .where(projects[:pending_delete].eq(true)) + .where(projects[:namespace_id].not_eq(nil)) + .skip(@offset * BATCH_SIZE) + .take(BATCH_SIZE) + .to_sql end end diff --git a/db/post_migrate/20170106142508_fill_authorized_projects.rb b/db/post_migrate/20170106142508_fill_authorized_projects.rb index 314c8440c8b..0ca20587981 100644 --- a/db/post_migrate/20170106142508_fill_authorized_projects.rb +++ b/db/post_migrate/20170106142508_fill_authorized_projects.rb @@ -15,8 +15,8 @@ class FillAuthorizedProjects < ActiveRecord::Migration disable_ddl_transaction! def up - relation = User.select(:id). - where('authorized_projects_populated IS NOT TRUE') + relation = User.select(:id) + .where('authorized_projects_populated IS NOT TRUE') relation.find_in_batches(batch_size: 1_000) do |rows| args = rows.map { |row| [row.id] } diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb index b1c9eed1148..01fff680183 100644 --- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -4,6 +4,8 @@ class ResetRelativePositionForIssue < ActiveRecord::Migration DOWNTIME = false + disable_ddl_transaction! + def up update_column_in_batches(:issues, :relative_position, nil) do |table, query| query.where(table[:relative_position].not_eq(nil)) @@ -11,5 +13,6 @@ class ResetRelativePositionForIssue < ActiveRecord::Migration end def down + # noop end end diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb index 44c688fa134..6a49450cc50 100644 --- a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb +++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb @@ -21,17 +21,17 @@ class RenameMoreReservedProjectNames < ActiveRecord::Migration private def reserved_projects - Project.unscoped. - includes(:namespace). - where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)'). - where('projects.path' => KNOWN_PATHS) + Project.unscoped + .includes(:namespace) + .where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)') + .where('projects.path' => KNOWN_PATHS) end def route_exists?(full_path) quoted_path = ActiveRecord::Base.connection.quote_string(full_path) - ActiveRecord::Base.connection. - select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present? + ActiveRecord::Base.connection + .select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present? end # Adds number to the end of the path that is not taken by other route diff --git a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb index 9a77b0bbdfb..ca2912f8dce 100644 --- a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb +++ b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb @@ -7,6 +7,8 @@ class UpdateUploadPathsToSystem < ActiveRecord::Migration DOWNTIME = false AFFECTED_MODELS = %w(User Project Note Namespace Appearance) + disable_ddl_transaction! + def up update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], base_directory, new_upload_dir)) do |_table, query| query.where(uploads_to_switch_to_new_path) diff --git a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb index 9ad36482c8a..397a9a2d28e 100644 --- a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb +++ b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb @@ -38,11 +38,11 @@ class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration activities = activities(day.at_beginning_of_day, day.at_end_of_day, page: page) update_sql = - Arel::UpdateManager.new(ActiveRecord::Base). - table(users_table). - set(users_table[:last_activity_on] => day.to_date). - where(users_table[:username].in(activities.map(&:first))). - to_sql + Arel::UpdateManager.new(ActiveRecord::Base) + .table(users_table) + .set(users_table[:last_activity_on] => day.to_date) + .where(users_table[:username].in(activities.map(&:first))) + .to_sql connection.exec_update(update_sql, self.class.name, []) diff --git a/db/post_migrate/20170406142253_migrate_user_project_view.rb b/db/post_migrate/20170406142253_migrate_user_project_view.rb index 22f0f2ac200..c4e910b3b44 100644 --- a/db/post_migrate/20170406142253_migrate_user_project_view.rb +++ b/db/post_migrate/20170406142253_migrate_user_project_view.rb @@ -7,6 +7,8 @@ class MigrateUserProjectView < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false + disable_ddl_transaction! + def up update_column_in_batches(:users, :project_view, 2) do |table, query| query.where(table[:project_view].eq(0)) diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb index 3c13a3d2518..765daa0a347 100644 --- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb +++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb @@ -7,6 +7,8 @@ class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration DOWNTIME = false def up + disable_statement_timeout + update_column_in_batches(:projects, :auto_cancel_pending_pipelines, 1) end diff --git a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb index ce52de91cdd..c1e64f20109 100644 --- a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb +++ b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb @@ -37,11 +37,11 @@ class CleanupNamespacelessPendingDeleteProjects < ActiveRecord::Migration def find_batch projects = Arel::Table.new(:projects) - projects.project(projects[:id]). - where(projects[:pending_delete].eq(true)). - where(projects[:namespace_id].eq(nil)). - skip(@offset * BATCH_SIZE). - take(BATCH_SIZE). - to_sql + projects.project(projects[:id]) + .where(projects[:pending_delete].eq(true)) + .where(projects[:namespace_id].eq(nil)) + .skip(@offset * BATCH_SIZE) + .take(BATCH_SIZE) + .to_sql end end diff --git a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb index bc3850c0c23..f77078ddd70 100644 --- a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb +++ b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb @@ -3,17 +3,19 @@ class AddHeadPipelineForEachMergeRequest < ActiveRecord::Migration DOWNTIME = false + disable_ddl_transaction! + def up disable_statement_timeout pipelines = Arel::Table.new(:ci_pipelines) merge_requests = Arel::Table.new(:merge_requests) - head_id = pipelines. - project(Arel::Nodes::NamedFunction.new('max', [pipelines[:id]])). - from(pipelines). - where(pipelines[:ref].eq(merge_requests[:source_branch])). - where(pipelines[:project_id].eq(merge_requests[:source_project_id])) + head_id = pipelines + .project(Arel::Nodes::NamedFunction.new('max', [pipelines[:id]])) + .from(pipelines) + .where(pipelines[:ref].eq(merge_requests[:source_branch])) + .where(pipelines[:project_id].eq(merge_requests[:source_project_id])) sub_query = Arel::Nodes::SqlLiteral.new(Arel::Nodes::Grouping.new(head_id).to_sql) diff --git a/db/post_migrate/20170526185901_remove_stage_id_index_from_builds.rb b/db/post_migrate/20170526185901_remove_stage_id_index_from_builds.rb new file mode 100644 index 00000000000..3879cf9133b --- /dev/null +++ b/db/post_migrate/20170526185901_remove_stage_id_index_from_builds.rb @@ -0,0 +1,18 @@ +class RemoveStageIdIndexFromBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + if index_exists?(:ci_builds, :stage_id) + remove_foreign_key(:ci_builds, column: :stage_id) + remove_concurrent_index(:ci_builds, :stage_id) + end + end + + def down + # noop + end +end diff --git a/db/post_migrate/20170526185921_migrate_build_stage_reference.rb b/db/post_migrate/20170526185921_migrate_build_stage_reference.rb index 797e106cae4..98c32d8284c 100644 --- a/db/post_migrate/20170526185921_migrate_build_stage_reference.rb +++ b/db/post_migrate/20170526185921_migrate_build_stage_reference.rb @@ -3,23 +3,17 @@ class MigrateBuildStageReference < ActiveRecord::Migration DOWNTIME = false - def up - disable_statement_timeout - - stage_id = Arel.sql <<-SQL.strip_heredoc - (SELECT id FROM ci_stages - WHERE ci_stages.pipeline_id = ci_builds.commit_id - AND ci_stages.name = ci_builds.stage) - SQL + ## + # This is an empty migration, content has been moved to a new one: + # post migrate 20170526190000 MigrateBuildStageReferenceAgain + # + # See gitlab-org/gitlab-ce!12337 for more details. - update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| - query.where(table[:stage_id].eq(nil)) - end + def up + # noop end def down - disable_statement_timeout - - update_column_in_batches(:ci_builds, :stage_id, nil) + # noop end end diff --git a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb new file mode 100644 index 00000000000..97cb242415d --- /dev/null +++ b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb @@ -0,0 +1,27 @@ +class MigrateBuildStageReferenceAgain < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + disable_statement_timeout + + stage_id = Arel.sql <<-SQL.strip_heredoc + (SELECT id FROM ci_stages + WHERE ci_stages.pipeline_id = ci_builds.commit_id + AND ci_stages.name = ci_builds.stage) + SQL + + update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| + query.where(table[:stage_id].eq(nil)) + end + end + + def down + disable_statement_timeout + + update_column_in_batches(:ci_builds, :stage_id, nil) + end +end diff --git a/db/post_migrate/20170607121233_convert_custom_notification_settings_to_columns.rb b/db/post_migrate/20170607121233_convert_custom_notification_settings_to_columns.rb new file mode 100644 index 00000000000..9abda6a1d73 --- /dev/null +++ b/db/post_migrate/20170607121233_convert_custom_notification_settings_to_columns.rb @@ -0,0 +1,55 @@ +class ConvertCustomNotificationSettingsToColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class NotificationSetting < ActiveRecord::Base + self.table_name = 'notification_settings' + + store :events, coder: JSON + end + + EMAIL_EVENTS = [ + :new_note, + :new_issue, + :reopen_issue, + :close_issue, + :reassign_issue, + :new_merge_request, + :reopen_merge_request, + :close_merge_request, + :reassign_merge_request, + :merge_merge_request, + :failed_pipeline, + :success_pipeline + ] + + # We only need to migrate (up or down) rows where at least one of these + # settings is set. + def up + NotificationSetting.where("events LIKE '%true%'").find_each do |notification_setting| + EMAIL_EVENTS.each do |event| + notification_setting[event] = notification_setting.events[event] + end + + notification_setting[:events] = nil + notification_setting.save! + end + end + + def down + NotificationSetting.where(EMAIL_EVENTS.join(' OR ')).find_each do |notification_setting| + events = {} + + EMAIL_EVENTS.each do |event| + events[event] = !!notification_setting.public_send(event) + notification_setting[event] = nil + end + + notification_setting[:events] = events + notification_setting.save! + end + end +end diff --git a/db/post_migrate/20170609183112_remove_position_from_issuables.rb b/db/post_migrate/20170609183112_remove_position_from_issuables.rb new file mode 100644 index 00000000000..4caaa2e83e8 --- /dev/null +++ b/db/post_migrate/20170609183112_remove_position_from_issuables.rb @@ -0,0 +1,8 @@ +class RemovePositionFromIssuables < ActiveRecord::Migration + DOWNTIME = false + + def change + remove_column :issues, :position, :integer + remove_column :merge_requests, :position, :integer + end +end diff --git a/db/post_migrate/20170621102400_add_stage_id_index_to_builds.rb b/db/post_migrate/20170621102400_add_stage_id_index_to_builds.rb new file mode 100644 index 00000000000..7d6609b18bf --- /dev/null +++ b/db/post_migrate/20170621102400_add_stage_id_index_to_builds.rb @@ -0,0 +1,21 @@ +class AddStageIdIndexToBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless index_exists?(:ci_builds, :stage_id) + add_concurrent_foreign_key(:ci_builds, :ci_stages, column: :stage_id, on_delete: :cascade) + add_concurrent_index(:ci_builds, :stage_id) + end + end + + def down + if index_exists?(:ci_builds, :stage_id) + remove_foreign_key(:ci_builds, column: :stage_id) + remove_concurrent_index(:ci_builds, :stage_id) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b93630a410d..8c7440ee610 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170606202615) do +ActiveRecord::Schema.define(version: 20170622162730) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "pg_trgm" @@ -123,6 +124,8 @@ ActiveRecord::Schema.define(version: 20170606202615) do t.boolean "clientside_sentry_enabled", default: false, null: false t.string "clientside_sentry_dsn" t.boolean "prometheus_metrics_enabled", default: false, null: false + t.boolean "help_page_hide_commercial_content", default: false + t.string "help_page_support_url" end create_table "audit_events", force: :cascade do |t| @@ -544,7 +547,6 @@ ActiveRecord::Schema.define(version: 20170606202615) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "position", default: 0 t.string "branch_name" t.text "description" t.integer "milestone_id" @@ -690,6 +692,22 @@ ActiveRecord::Schema.define(version: 20170606202615) do add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree + create_table "merge_request_diff_files", id: false, force: :cascade do |t| + t.integer "merge_request_diff_id", null: false + t.integer "relative_order", null: false + t.boolean "new_file", null: false + t.boolean "renamed_file", null: false + t.boolean "deleted_file", null: false + t.boolean "too_large", null: false + t.string "a_mode", null: false + t.string "b_mode", null: false + t.text "new_path", null: false + t.text "old_path", null: false + t.text "diff", null: false + end + + add_index "merge_request_diff_files", ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_files_on_mr_diff_id_and_order", unique: true, using: :btree + create_table "merge_request_diffs", force: :cascade do |t| t.string "state" t.text "st_commits" @@ -735,7 +753,6 @@ ActiveRecord::Schema.define(version: 20170606202615) do t.integer "target_project_id", null: false t.integer "iid" t.text "description" - t.integer "position", default: 0 t.datetime "locked_at" t.integer "updated_by_id" t.text "merge_error" @@ -753,6 +770,7 @@ ActiveRecord::Schema.define(version: 20170606202615) do t.datetime "last_edited_at" t.integer "last_edited_by_id" t.integer "head_pipeline_id" + t.boolean "ref_fetched" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -760,6 +778,7 @@ ActiveRecord::Schema.define(version: 20170606202615) do add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree @@ -875,6 +894,18 @@ ActiveRecord::Schema.define(version: 20170606202615) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "events" + t.boolean "new_note" + t.boolean "new_issue" + t.boolean "reopen_issue" + t.boolean "close_issue" + t.boolean "reassign_issue" + t.boolean "new_merge_request" + t.boolean "reopen_merge_request" + t.boolean "close_merge_request" + t.boolean "reassign_merge_request" + t.boolean "merge_merge_request" + t.boolean "failed_pipeline" + t.boolean "success_pipeline" end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree @@ -1517,6 +1548,7 @@ ActiveRecord::Schema.define(version: 20170606202615) do add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "lists", "boards" add_foreign_key "lists", "labels" + add_foreign_key "merge_request_diff_files", "merge_request_diffs", on_delete: :cascade add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index 9f12eed1471..fa755852304 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,12 +1,24 @@ -# GitLab Community Edition +# GitLab Documentation -[GitLab](https://about.gitlab.com/) is a Git-based fully featured platform -for software development. +Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured +platform for software development! -**GitLab Community Edition (CE)** is an opensource product, self-hosted, free to use. -All [GitLab products](https://about.gitlab.com/products/) contain the features -available in GitLab CE. Premium features are available in -[GitLab Enterprise Edition (EE)](https://about.gitlab.com/gitlab-ee/). +We offer four different products for you and your company: + +- **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/), +self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com. +- **GitLab Enterprise Edition (EE)** is an [opencore product](https://gitlab.com/gitlab-org/gitlab-ee/), +self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)** and **GitLab Enterprise Edition Premium (EEP)**. +- **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings). + +**GitLab EE** contains all features available in **GitLab CE**, +plus premium features available in each version: **Enterprise Edition Starter** +(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in +**EES** is also available in **EEP**. + +**Note:** _We are unifying the documentation for CE and EE. To check if certain feature is +available in CE or EE, look for a note right below the page title containing the GitLab +version which introduced that feature._ ---- @@ -24,7 +36,7 @@ Shortcuts to GitLab's most visited docs: - [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow. - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/). - [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown). -- [GitLab Slash Commands](user/project/slash_commands.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI. +- [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI. ### User account @@ -59,6 +71,7 @@ Manage files and branches from the UI (user interface): - Branches - [Create a branch](user/project/repository/web_editor.md#create-a-new-branch) - [Protected branches](user/project/protected_branches.md#protected-branches) + - [Delete merged branches](user/project/repository/branches/index.md#delete-merged-branches) ### Issues and Merge Requests (MRs) @@ -124,7 +137,7 @@ have access to GitLab administration tools and settings. - [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab - [Authentication/Authorization](topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers. -### GitLab admins' superpowers +### Features - [Container Registry](administration/container_registry.md): Configure Docker Registry with GitLab. - [Custom Git hooks](administration/custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough. diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index b6676026d06..9bcd13a52f7 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,6 +13,7 @@ override certain values. Variable | Type | Description -------- | ---- | ----------- +`GITLAB_CDN_HOST` | string | Sets the hostname for a CDN to serve static assets (e.g. `mycdnsubdomain.fictional-cdn.com`) `GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation `GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` @@ -58,6 +59,9 @@ to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. ## Omnibus configuration +To set environment variables, follow [these +instructions](https://docs.gitlab.com/omnibus/settings/environment-variables.html). + It's possible to preconfigure the GitLab docker image by adding the environment variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command. For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://docs.gitlab.com/omnibus/docker/#preconfigure-docker-container). diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 48929910a9c..332457cb384 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -33,6 +33,145 @@ prometheus_listen_addr = "localhost:9236" Changes to `/home/git/gitaly/config.toml` are applied when you run `service gitlab restart`. +## Running Gitaly on its own server + +> This is an optional way to deploy Gitaly which can benefit GitLab +installations that are larger than a single machine. Most +installations will be better served with the default configuration +used by Omnibus and the GitLab source installation guide. + +Starting with GitLab 9.4 it is possible to run Gitaly on a different +server from the rest of the application. This can improve performance +when running GitLab with its repositories stored on an NFS server. + +At the moment (GitLab 9.4) Gitaly is not yet a replacement for NFS +because some parts of GitLab still bypass Gitaly when accessing Git +repositories. If you choose to deploy Gitaly on your NFS server you +must still also mount your Git shares on your GitLab application +servers. + +Gitaly network traffic is unencrypted so you should use a firewall to +restrict access to your Gitaly server. + +Below we describe how to configure a Gitaly server at address +`gitaly.internal:9999` with secret token `abc123secret`. We assume +your GitLab installation has two repository storages, `default` and +`storage1`. + +### Client side token configuration + +Start by configuring a token on the client side. + +Omnibus installations: + +```ruby +# /etc/gitlab/gitlab.rb +gitlab_rails['gitaly_token'] = 'abc123secret' +``` + +Source installations: + +```yaml +# /home/git/gitlab/config/gitlab.yml +gitlab: + gitaly: + token: 'abc123secret' +``` + +You need to reconfigure (Omnibus) or restart (source) for these +changes to be picked up. + +### Gitaly server configuration + +Next, on the Gitaly server, we need to configure storage paths, enable +the network listener and configure the token. + +Note: if you want to reduce the risk of downtime when you enable +authentication you can temporarily disable enforcement, see [the +documentation on configuring Gitaly +authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication) +. + +In most or all cases the storage paths below end in `/repositories`. Check the +directory layout on your Gitaly server to be sure. + +Omnibus installations: + +```ruby +# /etc/gitlab/gitlab.rb +gitaly['listen_addr'] = '0.0.0.0:9999' +gitaly['auth_token'] = 'abc123secret' +gitaly['storage'] = [ + { 'name' => 'default', 'path' => '/path/to/default/repositories' }, + { 'name' => 'storage1', 'path' => '/path/to/storage1/repositories' }, +] +``` + +Source installations: + +```toml +# /home/git/gitaly/config.toml +listen_addr = '0.0.0.0:9999' + +[auth] +token = 'abc123secret' + +[[storage] +name = 'default' +path = '/path/to/default/repositories' + +[[storage]] +name = 'storage1' +path = '/path/to/storage1/repositories' +``` + +Again, reconfigure (Omnibus) or restart (source). + +### Converting clients to use the Gitaly server + +Now as the final step update the client machines to switch from using +their local Gitaly service to the new Gitaly server you just +configured. This is a risky step because if there is any sort of +network, firewall, or name resolution problem preventing your GitLab +server from reaching the Gitaly server then all Gitaly requests will +fail. + +We assume that your Gitaly server can be reached at +`gitaly.internal:9999` from your GitLab server, and that your GitLab +NFS shares are mounted at `/mnt/gitlab/default` and +`/mnt/gitlab/storage1` respectively. + +Omnibus installations: + +```ruby +# /etc/gitlab/gitlab.rb +git_data_dirs({ + { 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' } }, + { 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' } }, +}) +``` + +Source installations: + +```yaml +# /home/git/gitlab/config/gitlab.yml +gitlab: + repositories: + storages: + default: + path: /mnt/gitlab/default/repositories + gitaly_address: tcp://gitlab.internal:9999 + storage1: + path: /mnt/gitlab/storage1/repositories + gitaly_address: tcp://gitlab.internal:9999 +``` + +Now reconfigure (Omnibus) or restart (source). When you tail the +Gitaly logs on your Gitaly server (`sudo gitlab-ctl tail gitaly` or +`tail -f /home/git/gitlab/log/gitaly.log`) you should see requests +coming in. One sure way to trigger a Gitaly request is to clone a +repository from your GitLab server over HTTP. + ## Configuring GitLab to not use Gitaly Gitaly is still an optional component in GitLab 9.3. This means you diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index d8e76d6ab94..bd6b7327aed 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -1,12 +1,35 @@ # NFS -## Required NFS Server features +You can view information and options set for each of the mounted NFS file +systems by running `sudo nfsstat -m`. + +## NFS Server features + +### Required features **File locking**: GitLab **requires** advisory file locking, which is only supported natively in NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not specifically test NFSv3. +### Recommended options + +When you define your NFS exports, we recommend you also add the following +options: + +- `no_root_squash` - NFS normally changes the `root` user to `nobody`. This is + a good security measure when NFS shares will be accessed by many different + users. However, in this case only GitLab will use the NFS share so it + is safe. GitLab recommends the `no_root_squash` setting because we need to + manage file permissions automatically. Without the setting you may receive + errors when the Omnibus package tries to alter permissions. Note that GitLab + and other bundled components do **not** run as `root` but as non-privileged + users. The recommendation for `no_root_squash` is to allow the Omnibus package + to set ownership and permissions on files, as needed. +- `sync` - Force synchronous behavior. Default is asynchronous and under certain + circumstances it could lead to data loss if a failure occurs before data has + synced. + ## AWS Elastic File System GitLab does not recommend using AWS Elastic File System (EFS). @@ -26,27 +49,10 @@ GitLab does not recommend using EFS with GitLab. For more details on another person's experience with EFS, see [Amazon's Elastic File System: Burst Credits](https://www.rawkode.io/2017/04/amazons-elastic-file-system-burst-credits/) -### Recommended options - -When you define your NFS exports, we recommend you also add the following -options: - -- `no_root_squash` - NFS normally changes the `root` user to `nobody`. This is - a good security measure when NFS shares will be accessed by many different - users. However, in this case only GitLab will use the NFS share so it - is safe. GitLab recommends the `no_root_squash` setting because we need to - manage file permissions automatically. Without the setting you may receive - errors when the Omnibus package tries to alter permissions. Note that GitLab - and other bundled components do **not** run as `root` but as non-privileged - users. The recommendation for `no_root_squash` is to allow the Omnibus package - to set ownership and permissions on files, as needed. -- `sync` - Force synchronous behavior. Default is asynchronous and under certain - circumstances it could lead to data loss if a failure occurs before data has - synced. - ## NFS Client mount options -Below is an example of an NFS mount point we use on GitLab.com: +Below is an example of an NFS mount point defined in `/etc/fstab` we use on +GitLab.com: ``` 10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md index 3629772b8af..fe982ea83c2 100644 --- a/doc/administration/high_availability/redis_source.md +++ b/doc/administration/high_availability/redis_source.md @@ -364,3 +364,4 @@ When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics [downloads]: https://about.gitlab.com/downloads [restart]: ../restart_gitlab.md#installations-from-source [it]: https://gitlab.com/gitlab-org/gitlab-ce/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png +[resque]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/resque.yml.example diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 5599435564e..3587696225c 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -46,7 +46,10 @@ To disable artifacts site-wide, follow the steps below. After a successful job, GitLab Runner uploads an archive containing the job artifacts to GitLab. -To change the location where the artifacts are stored, follow the steps below. +### Using local storage + +To change the location where the artifacts are stored locally, follow the steps +below. --- @@ -82,6 +85,13 @@ _The artifacts are stored by default in 1. Save the file and [restart GitLab][] for the changes to take effect. +### Using object storage + +In [GitLab Enterprise Edition Premium][eep] you can use an object storage like +AWS S3 to store the artifacts. + +[Learn how to use the object storage option.][ee-os] + ## Expiring artifacts If an expiry date is used for the artifacts, they are marked for deletion @@ -148,3 +158,5 @@ memory and disk I/O. [reconfigure gitlab]: restart_gitlab.md "How to restart GitLab" [restart gitlab]: restart_gitlab.md "How to restart GitLab" [gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository" +[ee-os]: https://docs.gitlab.com/ee/administration/job_artifacts.html#using-object-storage +[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition Premium" diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md new file mode 100644 index 00000000000..07c05b5a6fb --- /dev/null +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -0,0 +1,47 @@ +# GitLab Prometheus metrics + +>**Note:** +Available since [Omnibus GitLab 9.3][29118]. Currently experimental. For installations from source +you'll have to configure it yourself. + +GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other [Prometheus] exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic. + +To enable the GitLab Prometheus metrics: + +1. Log into GitLab as an administrator, and go to the Admin area. +1. Click on the gear, then click on Settings. +1. Find the `Metrics - Prometheus` section, and click `Enable Prometheus Metrics` +1. [Restart GitLab][restart] for the changes to take effect + +## Collecting the metrics + +Since the metrics endpoint is available on the same host and port as other traffic, it requires authentication. The token and URL to access is displayed on the [Health Check][health-check] page. + +Currently the embedded Prometheus server is not automatically configured to collect metrics from this endpoint. We recommend setting up another Prometheus server, because the embedded server configuration is overwritten one every reconfigure of GitLab. In the future this will not be required. + +## Metrics available + +In this experimental phase, only a few metrics are available: + +| Metric | Type | Description | +| ------ | ---- | ----------- | +| db_ping_timeout | Gauge | Whether or not the last database ping timed out | +| db_ping_success | Gauge | Whether or not the last database ping succeeded | +| db_ping_latency | Gauge | Round trip time of the database ping | +| redis_ping_timeout | Gauge | Whether or not the last redis ping timed out | +| redis_ping_success | Gauge | Whether or not the last redis ping succeeded | +| redis_ping_latency | Gauge | Round trip time of the redis ping | +| filesystem_access_latency | gauge | Latency in accessing a specific filesystem | +| filesystem_accessible | gauge | Whether or not a specific filesystem is accessible | +| filesystem_write_latency | gauge | Write latency of a specific filesystem | +| filesystem_writable | gauge | Whether or not the filesystem is writable | +| filesystem_read_latency | gauge | Read latency of a specific filesystem | +| filesystem_readable | gauge | Whether or not the filesystem is readable | +| user_sessions_logins | Counter | Counter of how many users have logged in | + +[← Back to the main Prometheus page](index.md) + +[29118]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29118 +[Prometheus]: https://prometheus.io +[restart]: ../../restart_gitlab.md#omnibus-gitlab-restart +[health-check]: ../../../user/admin_area/monitoring/health_check.md diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md index edb9c911aac..f68b03d1ade 100644 --- a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md +++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md @@ -4,7 +4,7 @@ Available since [Omnibus GitLab 8.17][1132]. For installations from source you'll have to install and configure it yourself. -The [GitLab monitor exporter] allows you to measure various GitLab metrics. +The [GitLab monitor exporter] allows you to measure various GitLab metrics, pulled from Redis and the database. To enable the GitLab monitor exporter: diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index b2445d1c0e5..695fdf09a87 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -110,6 +110,14 @@ To disable the monitoring of Kubernetes: 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to take effect +## GitLab Prometheus metrics + +> Introduced as an experimental feature in GitLab 9.3. + +GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic. + +[➔ Read more about the GitLab Metrics.](gitlab_metrics.md) + ## Prometheus exporters There are a number of libraries and servers which help in exporting existing @@ -143,7 +151,7 @@ The Postgres exporter allows you to measure various PostgreSQL metrics. ### GitLab monitor exporter -The GitLab monitor exporter allows you to measure various GitLab metrics. +The GitLab monitor exporter allows you to measure various GitLab metrics, pulled from Redis and the database. [➔ Read more about the GitLab monitor exporter.](gitlab_monitor_exporter.md) diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md index affb4d17861..04c70c3644e 100644 --- a/doc/administration/raketasks/github_import.md +++ b/doc/administration/raketasks/github_import.md @@ -3,7 +3,7 @@ >**Note:** > > - [Introduced][ce-10308] in GitLab 9.1. -> - You need a personal access token in order to retrieve and import GitHub +> - You need a personal access token in order to retrieve and import GitHub > projects. You can get it from: https://github.com/settings/tokens > - You also need to pass an username as the second argument to the rake task > which will become the owner of the project. @@ -19,7 +19,7 @@ bundle exec rake import:github[access_token,root,foo/bar] RAILS_ENV=production ``` In this case, `access_token` is your GitHub personal access token, `root` -is your GitLab username, and `foo/bar` is the new GitLab namespace/project that +is your GitLab username, and `foo/bar` is the new GitLab namespace/project that will get created from your GitHub project. Subgroups are also possible: `foo/foo/bar`. diff --git a/doc/api/README.md b/doc/api/README.md index 1241801a81c..b7f6ee69193 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -29,10 +29,10 @@ following locations: - [Labels](labels.md) - [Merge Requests](merge_requests.md) - [Milestones](milestones.md) -- [Open source license templates](templates/licenses.md) - [Namespaces](namespaces.md) - [Notes](notes.md) (comments) - [Notification settings](notification_settings.md) +- [Open source license templates](templates/licenses.md) - [Pipelines](pipelines.md) - [Pipeline Triggers](pipeline_triggers.md) - [Pipeline Schedules](pipeline_schedules.md) @@ -55,6 +55,11 @@ following locations: - [V3 to V4](v3_to_v4.md) - [Version](version.md) +The following documentation is for the [internal CI API](ci/README.md): + +- [Builds](ci/builds.md) +- [Runners](ci/runners.md) + ## Road to GraphQL Going forward, we will start on moving to @@ -65,22 +70,20 @@ controller-specific endpoints. GraphQL has a number of benefits: 2. Callers of the API can request only what they need. 3. It is versioned by default. -It will co-exist with the current V4 REST API. If we have a V5 API, this should be -compatability layer on top of GraphQL. +It will co-exist with the current v4 REST API. If we have a v5 API, this should +be a compatibility layer on top of GraphQL. -### Internal CI API +## Authentication -The following documentation is for the [internal CI API](ci/README.md): +Most API requests require authentication via a session cookie or token. For +those cases where it is not required, this will be mentioned in the documentation +for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md). -- [Builds](ci/builds.md) -- [Runners](ci/runners.md) +There are three types of access tokens available: -## Authentication - -Most API requests require authentication via a session cookie or token. For those cases where it is not required, this will be mentioned in the documentation -for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md). -There are three types of tokens available: private tokens, OAuth 2 tokens, and personal -access tokens. +1. [OAuth2 tokens](#oauth2-tokens) +1. [Private tokens](#private-tokens) +1. [Personal access tokens](#personal-access-tokens) If authentication information is invalid or omitted, an error message will be returned with status code `401`: @@ -91,20 +94,13 @@ returned with status code `401`: } ``` -### Session Cookie +### Session cookie When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is set. The API will use this cookie for authentication if it is present, but using the API to generate a new session cookie is currently not supported. -### Private Tokens - -You need to pass a `private_token` parameter via query string or header. If passed as a -header, the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of -an underscore). You can find or reset your private token in your account page -(`/profile/account`). - -### OAuth 2 Tokens +### OAuth2 tokens You can use an OAuth 2 token to authenticate with the API by passing it either in the `access_token` parameter or in the `Authorization` header. @@ -117,30 +113,31 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api Read more about [GitLab as an OAuth2 client](oauth2.md). -### Personal Access Tokens +### Private tokens -> [Introduced][ce-3749] in GitLab 8.8. +Private tokens provide full access to the GitLab API. Anyone with access to +them can interact with GitLab as if they were you. You can find or reset your +private token in your account page (`/profile/account`). -You can create as many personal access tokens as you like from your GitLab -profile (`/profile/personal_access_tokens`); perhaps one for each application -that needs access to the GitLab API. +For examples of usage, [read the basic usage section](#basic-usage). -Once you have your token, pass it to the API using either the `private_token` -parameter or the `PRIVATE-TOKEN` header. +### Personal access tokens -> [Introduced][ce-5951] in GitLab 8.15. +Instead of using your private token which grants full access to your account, +personal access tokens could be a better fit because of their granular +permissions. -Personal Access Tokens can be created with one or more scopes that allow various actions -that a given token can perform. Although there are only two scopes available at the -moment – `read_user` and `api` – the groundwork has been laid to add more scopes easily. +Once you have your token, pass it to the API using either the `private_token` +parameter or the `PRIVATE-TOKEN` header. For examples of usage, +[read the basic usage section](#basic-usage). -At any time you can revoke any personal access token by just clicking **Revoke**. +[Read more about personal access tokens.][pat] ### Impersonation tokens > [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions. -Impersonation tokens are a type of [Personal Access Token](#personal-access-tokens) +Impersonation tokens are a type of [personal access token][pat] that can only be created by an admin for a specific user. They are a better alternative to using the user's password/private token @@ -149,9 +146,11 @@ or private token, since the password/token can change over time. Impersonation tokens are a great fit if you want to build applications or tools which authenticate with the API as a specific user. -For more information about the usage please refer to the +For more information, refer to the [users API](users.md#retrieve-user-impersonation-tokens) docs. +For examples of usage, [read the basic usage section](#basic-usage). + ### Sudo > Needs admin permissions. @@ -204,11 +203,16 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" ``` -## Basic Usage +## Basic usage API requests should be prefixed with `api` and the API version. The API version is defined in [`lib/api.rb`][lib-api-url]. +For endpoints that require [authentication](#authentication), you need to pass +a `private_token` parameter via query string or header. If passed as a header, +the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of +an underscore). + Example of a valid API request: ``` @@ -221,6 +225,12 @@ Example of a valid API request using cURL and authentication via header: curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" ``` +Example of a valid API request using cURL and authentication via a query string: + +```shell +curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK" +``` + The API uses JSON to serialize data. You don't need to specify `.json` at the end of an API URL. @@ -436,3 +446,4 @@ programming languages. Visit the [GitLab website] for a complete list. [ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749 [ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951 [ce-9099]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9099 +[pat]: ../user/profile/personal_access_tokens.md diff --git a/doc/api/branches.md b/doc/api/branches.md index 325d0ea4ce3..dfaa7d6fab7 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -251,6 +251,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gi Will delete all branches that are merged into the project's default branch. +Protected branches will not be deleted as part of this operation. + ``` DELETE /projects/:id/repository/merged_branches ``` diff --git a/doc/api/issues.md b/doc/api/issues.md index 3f949ca5667..df5666bb7b6 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -221,7 +221,8 @@ GET /projects/:id/issues?search=issue+title+or+description | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search project issues against their `title` and `description` | - +| `created_after` | datetime | no | Return issues created after the given time (inclusive) | +| `created_before` | datetime | no | Return issues created before the given time (inclusive) | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index cb22b67f556..3dc808c196d 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -26,6 +26,8 @@ Parameters: | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `milestone` | string | no | Return merge requests for a specific milestone | | `labels` | string | no | Return merge requests matching a comma separated list of labels | +| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) | +| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) | ```json [ diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 46fe64d382e..07cb64cb373 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -134,4 +134,4 @@ access_token = client.password.get_token('user@example.com', 'secret') puts access_token.token ``` -[personal access tokens]: ./README.md#personal-access-tokens
\ No newline at end of file +[personal access tokens]: ../user/profile/personal_access_tokens.md diff --git a/doc/api/projects.md b/doc/api/projects.md index 58f18105e21..cc1bb3911c8 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -261,6 +261,7 @@ Parameters: ], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, + "printing_merge_requests_link_enabled": true, "request_access_enabled": false, "statistics": { "commit_count": 37, @@ -344,6 +345,7 @@ Parameters: | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | +| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | ### Create project for user @@ -379,6 +381,7 @@ Parameters: | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | +| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | ### Edit project diff --git a/doc/api/session.md b/doc/api/session.md index 7dd504b67c5..f79eac11689 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -1,11 +1,9 @@ # Session API -## Deprecation Notice - -1. Starting in GitLab 8.11, this feature has been *disabled* for users with two-factor authentication turned on. -2. These users can access the API using [personal access tokens] instead. - ---- +>**Deprecation notice:** +Starting in GitLab 8.11, this feature has been **disabled** for users with +[two-factor authentication][2fa] turned on. These users can access the API +using [personal access tokens] instead. You can login with both GitLab and LDAP credentials in order to obtain the private token. @@ -52,4 +50,5 @@ Example response: } ``` -[personal access tokens]: ./README.md#personal-access-tokens +[2fa]: ../user/profile/account/two_factor_authentication.md +[personal access tokens]: ../user/profile/personal_access_tokens.md diff --git a/doc/api/users.md b/doc/api/users.md index f4167ba2605..cf09b8f44aa 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -62,6 +62,7 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", @@ -94,6 +95,7 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", "web_url": "http://localhost:3000/jack_smith", "created_at": "2012-05-23T08:01:01Z", + "is_admin": false, "bio": null, "location": null, "skype": "", @@ -197,6 +199,7 @@ Parameters: "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", @@ -251,6 +254,7 @@ Parameters: - `can_create_group` (optional) - User can create groups - true or false - `confirm` (optional) - Require confirmation - true (default) or false - `external` (optional) - Flags the user as external - true or false(default) +- `avatar` (optional) - Image file for user's avatar ## User modification @@ -279,6 +283,7 @@ Parameters: - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) +- `avatar` (optional) - Image file for user's avatar On password update, user will be forced to change it upon next login. Note, at the moment this method does only return a `404` error, @@ -804,7 +809,7 @@ Example response: It creates a new impersonation token. Note that only administrators can do this. You are only able to create impersonation tokens to impersonate the user and perform -both API calls and Git reads and writes. The user will not see these tokens in his profile +both API calls and Git reads and writes. The user will not see these tokens in their profile settings page. ``` diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 408d46a756c..f7c2a0ef0ca 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -282,9 +282,9 @@ which can be avoided if a different driver is used, for example `overlay`. > **Notes:** - This feature requires GitLab 8.8 and GitLab Runner 1.2. -- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need - to pass a personal access token instead of your password in order to login to - GitLab's Container Registry. +- Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need + to pass a [personal access token][pat] instead of your password in order to + login to GitLab's Container Registry. Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../user/project/container_registry.md). For example, @@ -409,3 +409,5 @@ Some things you should be aware of when using the Container Registry: [docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/ [docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities +[2fa]: ../../user/profile/account/two_factor_authentication.md +[pat]: ../../user/profile/personal_access_tokens.md diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 7709541ba9d..be4dea55c20 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -210,6 +210,18 @@ When the job is run, `tutum/wordpress` will be started and you will have access to it from your build container under the hostnames `tutum-wordpress` (requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`. +When using a private registry, the image name also includes a hostname and port +of the registry. + +```yaml +services: +- docker.example.com:5000/wordpress:latest +``` + +The service hostname will also include the registry hostname. Service will be +available under hostnames `docker.example.com-wordpress` (requires GitLab Runner v1.1.0 or newer) +and `docker.example.com__wordpress`. + *Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.* The alias hostnames for the service are made from the image name following these diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index a047e809788..5659a8c2a2a 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -27,7 +27,7 @@ download and analyze the report artifact in JSON format. For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically extracted and shown right in the merge request widget. [Learn more on code quality -diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md). +diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). [cli]: https://github.com/codeclimate/codeclimate [dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md index 8b0d8a003fd..b9f0485290e 100644 --- a/doc/ci/examples/deployment/composer-npm-deploy.md +++ b/doc/ci/examples/deployment/composer-npm-deploy.md @@ -20,12 +20,12 @@ before_script: - php -r "unlink('composer-setup.php');" ``` -This will make sure we have all requirements ready. Next, we want to run `composer update` to fetch all PHP dependencies and `npm install` to load node packages, then run the `npm` script. We need to append them into `before_script` section: +This will make sure we have all requirements ready. Next, we want to run `composer install` to fetch all PHP dependencies and `npm install` to load node packages, then run the `npm` script. We need to append them into `before_script` section: ```yaml before_script: # ... - - php composer.phar update + - php composer.phar install - npm install - npm run deploy ``` @@ -133,7 +133,7 @@ before_script: - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - php composer-setup.php - php -r "unlink('composer-setup.php');" - - php composer.phar update + - php composer.phar install - npm install - npm run deploy - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 41cae58782d..88e53ff40e8 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -155,7 +155,7 @@ Find more information about different Runners in the [Runners](../runners/README.md) documentation. You can find whether any Runners are assigned to your project by going to -**Settings ➔ CI/CD Pipelines**. Setting up a Runner is easy and straightforward. The +**Settings ➔ Pipelines**. Setting up a Runner is easy and straightforward. The official Runner supported by GitLab is written in Go and its documentation can be found at <https://docs.gitlab.com/runner/>. @@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as described in the next section. Once the Runner has been set up, you should see it on the Runners page of your -project, following **Settings ➔ CI/CD Pipelines**. +project, following **Settings ➔ Pipelines**. ![Activated runners](img/runners_activated.png) @@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can build any project. To enable the **Shared Runners** you have to go to your project's -**Settings ➔ CI/CD Pipelines** and click **Enable shared runners**. +**Settings ➔ Pipelines** and click **Enable shared runners**. [Read more on Shared Runners](../runners/README.md). diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 73aee3c3f56..76d746155eb 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -117,21 +117,21 @@ lowest number of jobs currently running on shared Runners. We have following jobs in queue: -- job 1 for project 1 -- job 2 for project 1 -- job 3 for project 1 -- job 4 for project 2 -- job 5 for project 2 -- job 6 for project 3 +- Job 1 for Project 1 +- Job 2 for Project 1 +- Job 3 for Project 1 +- Job 4 for Project 2 +- Job 5 for Project 2 +- Job 6 for Project 3 With the fair usage algorithm jobs are assigned in following order: -1. We choose job 1, because project 1 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs -1. We choose job 4, because project 2 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs -1. We choose job 6, because project 3 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs -1. We choose job 2, because project 1 as other it runs 1 job -1. We choose job 5, because project 2 runs 1 job, where project 1 runs 2 jobs now -1. We choose job 3, because project 1 and runs 2 jobs +1. Job 1 is chosen first, because it has the lowest job number from projects with no running jobs (i.e. all projects) +1. Job 4 is next, because 4 is now the lowest job number from projects with no running jobs (Project 1 has a job running) +1. Job 6 is next, because 6 is now the lowest job number from projects with no running jobs (Projects 1 and 2 have jobs running) +1. Job 2 is next, because, of projects with the lowest number of jobs running (each has 1), it is the lowest job number +1. Job 5 is next, because Project 1 now has 2 jobs running, and between Projects 2 and 3, Job 5 is the lowest remaining job number +1. Lastly we choose Job 3... because it's the only job left --- @@ -139,23 +139,23 @@ With the fair usage algorithm jobs are assigned in following order: We have following jobs in queue: -- job 1 for project 1 -- job 2 for project 1 -- job 3 for project 1 -- job 4 for project 2 -- job 5 for project 2 -- job 6 for project 3 +- Job 1 for project 1 +- Job 2 for project 1 +- Job 3 for project 1 +- Job 4 for project 2 +- Job 5 for project 2 +- Job 6 for project 3 With the fair usage algorithm jobs are assigned in following order: -1. We choose job 1, because project 1 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs +1. Job 1 is chosen first, because it has the lowest job number from projects with no running jobs (i.e. all projects) 1. We finish job 1 -1. We choose job 2, because project 1 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs -1. We choose job 4, because project 2 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs +1. Job 2 is next, because, having finished Job 1, all projects have 0 jobs running again, and 2 is the lowest available job number +1. Job 4 is next, because with Project 1 running a job, 4 is the lowest number from projects running no jobs (Projects 2 and 3) 1. We finish job 4 -1. We choose job 5, because project 2 doesn't run currently any jobs and has the lowest job number from projects that doesn't run jobs -1. We choose job 6, because project 3 doesn't run currently any jobs -1. We choose job 3, because project 1, 2 and 3 runs exactly one job now +1. Job 5 is next, because having finished Job 4, Project 2 has no jobs running again +1. Job 6 is next, because Project 3 is the only project left with no running jobs +1. Lastly we choose Job 3... because, again, it's the only job left (who says 1 is the loneliest number?) ## Using shared Runners effectively diff --git a/doc/development/architecture.md b/doc/development/architecture.md index acd5e3c2093..54029e00507 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -194,3 +194,7 @@ bundle exec rake gitlab:check RAILS_ENV=production ``` Note: It is recommended to log into the `git` user using `sudo -i -u git` or `sudo su - git`. While the sudo commands provided by gitlabhq work in Ubuntu they do not always work in RHEL. + +## GitLab.com + +We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/) but this is probably over the top unless you have millions of users. diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index d2d89517241..ae844fa1051 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -463,20 +463,24 @@ A forEach will cause side effects, it will be mutating the array being iterated. 1. `destroyed` #### Vue and Boostrap -1. Tooltips: Do not rely on `has-tooltip` class name for vue components +1. Tooltips: Do not rely on `has-tooltip` class name for Vue components ```javascript // bad - <span class="has-tooltip"> + <span + class="has-tooltip" + title="Some tooltip text"> Text </span> // good - <span data-toggle="tooltip"> + <span + v-tooltip + title="Some tooltip text"> Text </span> ``` -1. Tooltips: When using a tooltip, include the tooltip mixin +1. Tooltips: When using a tooltip, include the tooltip directive, `./app/assets/javascripts/vue_shared/directives/tooltip.js` 1. Don't change `data-original-title`. ```javascript diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index 51b4b398f2c..899be9eae4b 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -166,8 +166,8 @@ For instance this kind of thing: = render 'projects/zen', f: form, attr: :description, classes: 'note-textarea', placeholder: "Write a comment or drag your files here...", - supports_slash_commands: !issuable.persisted? - = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + supports_quick_actions: !issuable.persisted? + = render 'projects/notes/hints', supports_quick_actions: !issuable.persisted? .clearfix .error-alert - if issuable.is_a?(Issue) diff --git a/doc/development/testing.md b/doc/development/testing.md index 6d8b846d27f..cf3ea2ccfc2 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -25,7 +25,7 @@ records should use stubs/doubles as much as possible. | --------- | ---------- | -------------- | ----- | | `app/finders/` | `spec/finders/` | RSpec | | | `app/helpers/` | `spec/helpers/` | RSpec | | -| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | | +| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). | | `app/policies/` | `spec/policies/` | RSpec | | | `app/presenters/` | `spec/presenters/` | RSpec | | | `app/routing/` | `spec/routing/` | RSpec | | diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 197a92905c8..a3d676433e6 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -86,56 +86,32 @@ if your available memory changes. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. -## GitLab Runner - -We strongly advise against installing GitLab Runner on the same machine you plan -to install GitLab on. Depending on how you decide to configure GitLab Runner and -what tools you use to exercise your application in the CI environment, GitLab -Runner can consume significant amount of available memory. - -Memory consumption calculations, that are available above, will not be valid if -you decide to run GitLab Runner and the GitLab Rails application on the same -machine. - -It is also not safe to install everything on a single machine, because of the -[security reasons] - especially when you plan to use shell executor with GitLab -Runner. - -We recommend using a separate machine for each GitLab Runner, if you plan to -use the CI features. - -[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md - -## Unicorn Workers - -It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests. - -For most instances we recommend using: CPU cores + 1 = unicorn workers. -So for a machine with 2 cores, 3 unicorn workers is ideal. +## Database -For all machines that have 2GB and up we recommend a minimum of three unicorn workers. -If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. +The server running the database should have _at least_ 5-10 GB of storage +available, though the exact requirements depend on the size of the GitLab +installation (e.g. the number of users, projects, etc). -To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). +We currently support the following databases: -## Database +- PostgreSQL (highly recommended) +- MySQL/MariaDB (strongly discouraged, not all GitLab features are supported, no support for [MySQL/MariaDB GTID](https://mariadb.com/kb/en/mariadb/gtid/)) -We currently support the following databases: +We highly recommend the use of PostgreSQL instead of MySQL/MariaDB as not all +features of GitLab work with MySQL/MariaDB: -- PostgreSQL -- MySQL/MariaDB +1. MySQL support for subgroups was [dropped with GitLab 9.3][post]. + See [issue #30472][30472] for more information. +1. GitLab Geo does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). +1. [Zero downtime migrations][zero] do not work with MySQL +1. We expect this list to grow over time. -We _highly_ recommend the use of PostgreSQL instead of MySQL/MariaDB as not all -features of GitLab may work with MySQL/MariaDB. For example, MySQL does not have -the right features to support nested groups in an efficient manner; see -<https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> for more information -about this. GitLab Geo also does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). Existing users using GitLab with MySQL/MariaDB are advised to -migrate to PostgreSQL instead. +[migrate to PostgreSQL](../update/mysql_to_postgresql.md) instead. -The server running the database should have _at least_ 5-10 GB of storage -available, though the exact requirements depend on the size of the GitLab -installation (e.g. the number of users, projects, etc). +[30472]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472 +[zero]: ../update/README.md#upgrading-without-downtime +[post]: https://about.gitlab.com/2017/06/22/gitlab-9-3-released/#dropping-support-for-subgroups-in-mysql ### PostgreSQL Requirements @@ -154,6 +130,18 @@ CREATE EXTENSION pg_trgm; On some systems you may need to install an additional package (e.g. `postgresql-contrib`) for this extension to become available. +## Unicorn Workers + +It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests. + +For most instances we recommend using: CPU cores + 1 = unicorn workers. +So for a machine with 2 cores, 3 unicorn workers is ideal. + +For all machines that have 2GB and up we recommend a minimum of three unicorn workers. +If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. + +To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). + ## Redis and Sidekiq Redis stores all user sessions and the background task queue. @@ -172,6 +160,26 @@ default settings. If you would like to disable Prometheus and it's exporters or read more information about it, check the [Prometheus documentation](../administration/monitoring/prometheus/index.md). +## GitLab Runner + +We strongly advise against installing GitLab Runner on the same machine you plan +to install GitLab on. Depending on how you decide to configure GitLab Runner and +what tools you use to exercise your application in the CI environment, GitLab +Runner can consume significant amount of available memory. + +Memory consumption calculations, that are available above, will not be valid if +you decide to run GitLab Runner and the GitLab Rails application on the same +machine. + +It is also not safe to install everything on a single machine, because of the +[security reasons] - especially when you plan to use shell executor with GitLab +Runner. + +We recommend using a separate machine for each GitLab Runner, if you plan to +use the CI features. + +[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md + ## Supported web browsers We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11). diff --git a/doc/integration/chat_commands.md b/doc/integration/chat_commands.md index c878dc7e650..2856992ee25 100644 --- a/doc/integration/chat_commands.md +++ b/doc/integration/chat_commands.md @@ -1,14 +1 @@ -# Chat Commands - -Chat commands in Mattermost and Slack (also called Slack slash commands) allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it. - -Commands are scoped to a project, with a trigger term that is specified during configuration. (We suggest you use the project name as the trigger term for simplicty and clarity.) Taking the trigger term as `project-name`, the commands are: - - -| Command | Effect | -| ------- | ------ | -| `/project-name help` | Shows all available chat commands | -| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` | -| `/project-name issue show <id>` | Shows the issue with id `<id>` | -| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` | -| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
\ No newline at end of file +This document was moved to [integration/slash_commands.md](slash_commands.md). diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md new file mode 100644 index 00000000000..5d880ba785c --- /dev/null +++ b/doc/integration/slash_commands.md @@ -0,0 +1,14 @@ +# Slash Commands + +Slash commands in Mattermost and Slack allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it. + +Commands are scoped to a project, with a trigger term that is specified during configuration. (We suggest you use the project name as the trigger term for simplicty and clarity.) Taking the trigger term as `project-name`, the commands are: + + +| Command | Effect | +| ------- | ------ | +| `/project-name help` | Shows all available slash commands | +| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` | +| `/project-name issue show <id>` | Shows the issue with id `<id>` | +| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` | +| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment | diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md index 0c32e4db53f..8fbcc892fd5 100644 --- a/doc/update/9.2-to-9.3.md +++ b/doc/update/9.2-to-9.3.md @@ -117,7 +117,7 @@ cd /home/git/gitlab sudo -u git -H git checkout 9-3-stable-ee ``` -### 5. Update gitlab-shell +### 7. Update gitlab-shell ```bash cd /home/git/gitlab-shell @@ -127,7 +127,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) sudo -u git -H bin/compile ``` -### 6. Update gitlab-workhorse +### 8. Update gitlab-workhorse Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl) which should already be on your system from @@ -143,7 +143,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) sudo -u git -H make ``` -### 7. Update Gitaly +### 9. Update Gitaly If you have not yet set up Gitaly then follow [Gitaly section of the installation guide](../install/installation.md#install-gitaly). diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md new file mode 100644 index 00000000000..a712ce5a8b1 --- /dev/null +++ b/doc/update/9.3-to-9.4.md @@ -0,0 +1,317 @@ +# From 9.3 to 9.4 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --location https://yarnpkg.com/install.sh | bash - +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.4 and higher only supports Go 1.8.3 and dropped support for Go 1.5.x through 1.7.x. Be +sure to upgrade your installation if necessary + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-4-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-4-stable-ee +``` + +### 5. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 6. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.8](https://golang.org/dl) which should already be on your system from +GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 7. Update Gitaly + +If you have not yet set up Gitaly then follow [Gitaly section of the installation +guide](../install/installation.md#install-gitaly). + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +cd /home/git/gitaly +sudo -u git -H editor config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/9-3-stable:config/gitlab.yml.example origin/9-4-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/9-3-stable:lib/support/nginx/gitlab-ssl origin/9-4-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/9-3-stable:lib/support/nginx/gitlab origin/9-4-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/9-3-stable:lib/support/init.d/gitlab.default.example origin/9-4-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 11. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 12. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 13. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (9.3) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 9.2 to 9.3](9.2-to-9.3.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/update/README.md b/doc/update/README.md index d024a809f24..22dbc7c750f 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -11,22 +11,6 @@ There are currently 3 official ways to install GitLab: Based on your installation, choose a section below that fits your needs. ---- - -<!-- START doctoc generated TOC please keep comment here to allow auto update --> -<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Omnibus Packages](#omnibus-packages) -- [Installation from source](#installation-from-source) -- [Installation using Docker](#installation-using-docker) -- [Upgrading between editions](#upgrading-between-editions) - - [Community to Enterprise Edition](#community-to-enterprise-edition) - - [Enterprise to Community Edition](#enterprise-to-community-edition) -- [Miscellaneous](#miscellaneous) - -<!-- END doctoc generated TOC please keep comment here to allow auto update --> - ## Omnibus Packages - The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) diff --git a/doc/user/admin_area/monitoring/convdev.md b/doc/user/admin_area/monitoring/convdev.md new file mode 100644 index 00000000000..3d93c7557a4 --- /dev/null +++ b/doc/user/admin_area/monitoring/convdev.md @@ -0,0 +1,29 @@ +# Conversational Development Index + +> [Introduced][ce-30469] in GitLab 9.3. + +Conversational Development Index (ConvDev) gives you an overview of your entire +instance's feature usage, from idea to production. It looks at your usage in the +past 30 days, averaged over the number of active users in that time period. It also +provides a lead score per feature, which is calculated based on GitLab's analysis +of top performing instances, based on [usage ping data][ping] that GitLab has +collected. Your score is compared to the lead score, expressed as a percentage. +The overall index score is an average over all your feature scores. + +![ConvDev index](img/convdev_index.png) + +The page also provides helpful links to articles and GitLab docs, to help you +improve your scores. + +Your GitLab instance's usage ping must be activated in order to use this feature. +Usage ping data is aggregated on GitLab's servers for analysis. Your usage +information is **not sent** to any other GitLab instances. + +If you have just started using GitLab, it may take a few weeks for data to be +collected before this feature is available. + +This feature is accessible only to a system admin, at +**Admin area > Monitoring > ConvDev Index**. + +[ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469 +[ping]: ../settings/usage_statistics.md#usage-ping diff --git a/doc/user/admin_area/monitoring/img/convdev_index.png b/doc/user/admin_area/monitoring/img/convdev_index.png Binary files differnew file mode 100644 index 00000000000..4e47ff2228d --- /dev/null +++ b/doc/user/admin_area/monitoring/img/convdev_index.png diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index f3745d0efa7..d874688cc29 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -3,7 +3,8 @@ GitLab Inc. will periodically collect information about your instance in order to perform various actions. -All statistics are opt-out, you can disable them from the admin panel. +All statistics are opt-out, you can enable/disable them from the admin panel +under **Admin area > Settings > Usage statistics**. ## Version check diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 59e343ebe51..8b1d299484c 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -10,7 +10,7 @@ You can leave a comment in the following places: - commits - commit diffs -The comment area supports [Markdown] and [slash commands]. One can edit their +The comment area supports [Markdown] and [quick actions]. One can edit their own comment at any time, and anyone with [Master access level][permissions] or higher can also edit a comment made by someone else. @@ -146,5 +146,5 @@ comments in greater detail. [discussion-view]: img/discussion_view.png [discussions-resolved]: img/discussions_resolved.png [markdown]: ../markdown.md -[slash commands]: ../project/slash_commands.md +[quick actions]: ../project/quick_actions.md [permissions]: ../permissions.md diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index c4921c74a17..5724dcfab48 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -1,6 +1,9 @@ # Subgroups -> [Introduced][ce-2772] in GitLab 9.0. +>**Notes:** +- [Introduced][ce-2772] in GitLab 9.0. +- Not available when using MySQL as external database (support removed in + GitLab 9.3 [due to performance reasons][issue]). With subgroups (aka nested groups or hierarchical groups) you can have up to 20 levels of nested groups, which among other things can help you to: @@ -173,3 +176,4 @@ Here's a list of what you can't do with subgroups: [ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 [permissions]: ../../permissions.md#group [reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb +[issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600 diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 3fda47b9e34..3d47e644ad2 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -89,6 +89,7 @@ group. | Create project in group | | | | ✓ | ✓ | | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | +| Manage group labels | | ✓ | ✓ | ✓ | ✓ | ## External Users diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index fb69d934ae1..590c3f862fb 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -125,23 +125,14 @@ applications and U2F devices. ## Personal access tokens When 2FA is enabled, you can no longer use your normal account password to -authenticate with Git over HTTPS on the command line, you must use a personal -access token instead. - -1. Log in to your GitLab account. -1. Go to your **Profile Settings**. -1. Go to **Access Tokens**. -1. Choose a name and expiry date for the token. -1. Click on **Create Personal Access Token**. -1. Save the personal access token somewhere safe. - -When using Git over HTTPS on the command line, enter the personal access token -into the password field. +authenticate with Git over HTTPS on the command line or when using +[GitLab's API][api], you must use a [personal access token][pat] instead. ## Recovery options To disable two-factor authentication on your account (for example, if you have lost your code generation device) you can: + * [Use a saved recovery code](#use-a-saved-recovery-code) * [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-ssh) * [Ask a GitLab administrator to disable two-factor authentication on your account](#ask-a-gitlab-administrator-to-disable-two-factor-authentication-on-your-account) @@ -154,8 +145,9 @@ codes. If you saved these codes, you can use one of them to sign in. To use a recovery code, enter your username/email and password on the GitLab sign-in page. When prompted for a two-factor code, enter the recovery code. -> **Note:** Once you use a recovery code, you cannot re-use it. You can still - use the other recovery codes you saved. +>**Note:** +Once you use a recovery code, you cannot re-use it. You can still use the other +recovery codes you saved. ### Generate new recovery codes using SSH @@ -190,11 +182,14 @@ a new set of recovery codes with SSH. two-factor code. Then, visit your Profile Settings and add a new device so you do not lose access to your account again. ``` -3. Go to the GitLab sign-in page and enter your username/email and password. When prompted for a two-factor code, enter one of the recovery codes obtained -from the command-line output. -> **Note:** After signing in, visit your **Profile Settings -> Account** immediately to set up two-factor authentication with a new - device. +3. Go to the GitLab sign-in page and enter your username/email and password. + When prompted for a two-factor code, enter one of the recovery codes obtained + from the command-line output. + +>**Note:** +After signing in, visit your **Profile settings > Account** immediately to set +up two-factor authentication with a new device. ### Ask a GitLab administrator to disable two-factor authentication on your account @@ -206,23 +201,23 @@ Sign in and re-enable two-factor authentication as soon as possible. ## Note to GitLab administrators - You need to take special care to that 2FA keeps working after -[restoring a GitLab backup](../../../raketasks/backup_restore.md). - + [restoring a GitLab backup](../../../raketasks/backup_restore.md). - To ensure 2FA authorizes correctly with TOTP server, you may want to ensure -your GitLab server's time is synchronized via a service like NTP. Otherwise, -you may have cases where authorization always fails because of time differences. - -[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en -[FreeOTP]: https://freeotp.github.io/ -[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ - + your GitLab server's time is synchronized via a service like NTP. Otherwise, + you may have cases where authorization always fails because of time differences. - The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from -multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at -the time of registration, and cannot be used for other hostnames/FQDNs. + multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at + the time of registration, and cannot be used for other hostnames/FQDNs. For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: - The user logs in via `first.host.xyz` and registers their U2F key. - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds. - - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because + - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because the U2F key has only been registered on `first.host.xyz`. + +[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en +[FreeOTP]: https://freeotp.github.io/ +[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ +[api]: ../../../api/README.md +[pat]: ../personal_access_tokens.md diff --git a/doc/user/profile/img/personal_access_tokens.png b/doc/user/profile/img/personal_access_tokens.png Binary files differnew file mode 100644 index 00000000000..6aa63dbe342 --- /dev/null +++ b/doc/user/profile/img/personal_access_tokens.png diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md new file mode 100644 index 00000000000..9488ce1ef30 --- /dev/null +++ b/doc/user/profile/personal_access_tokens.md @@ -0,0 +1,57 @@ +# Personal access tokens + +> [Introduced][ce-3749] in GitLab 8.8. + +Personal access tokens are useful if you need access to the [GitLab API][api]. +Instead of using your private token which grants full access to your account, +personal access tokens could be a better fit because of their +[granular permissions](#limiting-scopes-of-a-personal-access-token). + +You can also use them to authenticate against Git over HTTP. They are the only +accepted method of authentication when you have +[Two-Factor Authentication (2FA)][2fa] enabled. + +Once you have your token, [pass it to the API][usage] using either the +`private_token` parameter or the `PRIVATE-TOKEN` header. + +## Creating a personal access token + +You can create as many personal access tokens as you like from your GitLab +profile. + +1. Log in to your GitLab account. +1. Go to your **Profile settings**. +1. Go to **Access tokens**. +1. Choose a name and optionally an expiry date for the token. +1. Choose the [desired scopes](#limiting-scopes-of-a-personal-access-token). +1. Click on **Create personal access token**. +1. Save the personal access token somewhere safe. Once you leave or refresh + the page, you won't be able to access it again. + +![Personal access tokens page](img/personal_access_tokens.png) + +## Revoking a personal access token + +At any time, you can revoke any personal access token by just clicking the +respective **Revoke** button under the 'Active personal access tokens' area. + +## Limiting scopes of a personal access token + +Personal access tokens can be created with one or more scopes that allow various +actions that a given token can perform. The available scopes are depicted in +the following table. + +| Scope | Description | +| ----- | ----------- | +|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). | +| `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. | +| `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). | + +[2fa]: ../account/two_factor_authentication.md +[api]: ../../api/README.md +[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749 +[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951 +[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 +[container registry]: ../project/container_registry.md +[users]: ../../api/users.md +[usage]: ../../api/README.md#basic-usage diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 75ea911b9bc..629d69d8aea 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -8,8 +8,8 @@ Registry across your GitLab instance, visit the [administrator documentation](../../administration/container_registry.md). - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need - to pass a personal access token instead of your password in order to login to - GitLab's Container Registry. + to pass a [personal access token][pat] instead of your password in order to + login to GitLab's Container Registry. - Multiple level image names support was added in GitLab 9.1 With the Docker Container Registry integrated into GitLab, every project can @@ -114,12 +114,11 @@ and [Using the GitLab Container Registry documentation](../../ci/docker/using_do ## Using with private projects -If a project is private, credentials will need to be provided for authorization. -The preferred way to do this, is by using personal access tokens, which can be -created under `/profile/personal_access_tokens`. The minimal scope needed is: -`read_registry`. +> [Introduced][ce-11845] in GitLab 9.3. -This feature was introduced in GitLab 9.3. +If a project is private, credentials will need to be provided for authorization. +The preferred way to do this, is by using [personal access tokens][pat]. +The minimal scope needed is `read_registry`. ## Troubleshooting the GitLab Container Registry @@ -264,4 +263,6 @@ The solution: check the [IAM permissions again](https://docs.docker.com/registry Once the right permissions were set, the error will go away. [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ +[pat]: ../profile/personal_access_tokens.md diff --git a/doc/user/project/integrations/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png Binary files differindex c74351b57b8..e69376f74c4 100644 --- a/doc/user/project/integrations/img/jira_service_page.png +++ b/doc/user/project/integrations/img/jira_service_page.png diff --git a/doc/user/project/integrations/img/merge_request_performance.png b/doc/user/project/integrations/img/merge_request_performance.png Binary files differindex 93b2626fed7..eba6515a6ae 100644 --- a/doc/user/project/integrations/img/merge_request_performance.png +++ b/doc/user/project/integrations/img/merge_request_performance.png diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index a048260b033..cf03f2a9033 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -76,7 +76,7 @@ We have split this stage in steps so it is easier to follow. ![JIRA add user to group](img/jira_add_user_to_group.png) ---- + --- The JIRA configuration is over. Write down the new JIRA username and its password as they will be needed when configuring GitLab in the next section. @@ -98,14 +98,14 @@ in the table below. | Field | Description | | ----- | ----------- | | `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | -| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | -| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | +| `JIRA API URL` | The base URL to the JIRA instance API. E.g., `https://jira-api.example.com`. This is optional. If not entered, the Web URL value be used. | +| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. +with all JIRA projects in your JIRA instance. ![JIRA service page](img/jira_service_page.png) diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index d3fb5916dc6..86ceb14b965 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -167,15 +167,15 @@ environment which has had a successful deployment. ## Determining the performance impact of a merge > [Introduced][ce-10408] in GitLab 9.2. +> GitLab 9.3 added the [numeric comparison](https://gitlab.com/gitlab-org/gitlab-ce/issues/27439) of the 30 minute averages. Developers can view the performance impact of their changes within the merge -request workflow. When a source branch has been deployed to an environment, a -sparkline will appear showing the average memory consumption of the app. The dot +request workflow. When a source branch has been deployed to an environment, a sparkline and numeric comparison of the average memory consumption will appear. On the sparkline, a dot indicates when the current changes were deployed, with up to 30 minutes of -performance data displayed before and after. The sparkline will be updated after +performance data displayed before and after. The comparison shows the difference between the 30 minute average before and after the deployment. This information is updated after each commit has been deployed. -Once merged and the target branch has been redeployed, the sparkline will switch +Once merged and the target branch has been redeployed, the metrics will switch to show the new environments this revision has been deployed to. Performance data will be available for the duration it is persisted on the diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index 54e0ee611cb..c267da69bb3 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -2,7 +2,7 @@ > Introduced in GitLab 8.15 -Slack slash commands (also known as chat commmands) allow you to control GitLab and view content right inside Slack, without having to leave it. This requires configurations in both Slack and GitLab. +Slack slash commands allow you to control GitLab and view content right inside Slack, without having to leave it. This requires configurations in both Slack and GitLab. > Note: GitLab can also send events (e.g. issue created) to Slack as notifications. This is the separately configured [Slack Notifications Service](slack.md). @@ -20,4 +20,4 @@ Slack slash commands (also known as chat commmands) allow you to control GitLab ## Usage -You can now use the [Slack slash commands](../../../integration/chat_commands.md).
\ No newline at end of file +You can now use the [Slack slash commands](../../../integration/slash_commands.md). diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 0517ed3ec18..023c6932e41 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -736,7 +736,7 @@ X-Gitlab-Event: Merge Request Hook ### Wiki Page events -Triggered when a wiki page is created, edited or deleted. +Triggered when a wiki page is created, updated or deleted. **Request Header**: diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 5aa8337b75d..ebea7062ecb 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -31,10 +31,11 @@ Below is a table of the definitions used for GitLab's Issue Board. | **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. You can re-order cards within a list. | There are two types of lists, the ones you create based on your labels, and -one default: +two defaults: - Label list: a list based on a label. It shows all opened issues with that label. -- **Done** (default): shows all closed issues. Always appears on the very right. +- **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left. +- **Closed** (default): shows all closed issues. Always appears on the very right. ![GitLab Issue Board](img/issue_board.png) diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md index ba843201e1a..294176e61f9 100644 --- a/doc/user/project/issues/issues_functionalities.md +++ b/doc/user/project/issues/issues_functionalities.md @@ -68,7 +68,7 @@ This feature is available only in [GitLab Enterprise Edition](https://about.gitl - Spend: add the time spent on the implementation of that issue > **Note:** -both estimate and spend times are set via [GitLab Slash Commands](../slash_commands.md). +both estimate and spend times are set via [GitLab Quick Actions](../quick_actions.md). Learn more on the [Time Tracking documentation](https://docs.gitlab.com/ee/workflow/time_tracking.html). @@ -147,7 +147,7 @@ or in the issue thread. #### 15. Award emoji -- Award an emoji to that issue. +- Award an emoji to that issue. > **Tip:** Posting "+1" as comments in threads spam all diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index e9512497d6c..271adee7da1 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -212,9 +212,9 @@ Container Registries for private projects. access token created explicitly for this purpose). This issue is resolved with latest changes in GitLab Runner 1.8 which receives GitLab credentials with build data. -- Starting with GitLab 8.12, if you have 2FA enabled in your account, you need - to pass a personal access token instead of your password in order to login to - GitLab's Container Registry. +- Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need + to pass a [personal access token][pat] instead of your password in order to + login to GitLab's Container Registry. Your jobs can access all container images that you would normally have access to. The only implication is that you can push to the Container Registry of the @@ -239,3 +239,5 @@ test: [update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse [jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables +[2fa]: ../profile/account/two_factor_authentication.md +[pat]: ../profile/personal_access_tokens.md diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 1d2eba4f74b..a992a348c0f 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -1,7 +1,7 @@ # Pipelines settings To reach the pipelines settings navigate to your project's -**Settings ➔ CI/CD Pipelines**. +**Settings ➔ Pipelines**. The following settings can be configured per project. diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md new file mode 100644 index 00000000000..19b51c83222 --- /dev/null +++ b/doc/user/project/quick_actions.md @@ -0,0 +1,39 @@ +# GitLab quick actions + +Quick actions are textual shortcuts for common actions on issues or merge +requests that are usually done by clicking buttons or dropdowns in GitLab's UI. +You can enter these commands while creating a new issue or merge request, and +in comments. Each command should be on a separate line in order to be properly +detected and executed. The commands are removed from the issue, merge request or +comment body before it is saved and will not be visible to anyone else. + +Below is a list of all of the available commands and descriptions about what they +do. + +| Command | Action | +|:---------------------------|:-------------| +| `/close` | Close the issue or merge request | +| `/reopen` | Reopen the issue or merge request | +| `/merge` | Merge (when pipeline succeeds) | +| `/title <New title>` | Change title | +| `/assign @username` | Assign | +| `/unassign` | Remove assignee | +| `/milestone %milestone` | Set milestone | +| `/remove_milestone` | Remove milestone | +| `/label ~foo ~"bar baz"` | Add label(s) | +| `/unlabel ~foo ~"bar baz"` | Remove all or specific label(s) | +| `/relabel ~foo ~"bar baz"` | Replace all label(s) | +| `/todo` | Add a todo | +| `/done` | Mark todo as done | +| `/subscribe` | Subscribe | +| `/unsubscribe` | Unsubscribe | +| <code>/due <in 2 days | this Friday | December 31st></code> | Set due date | +| `/remove_due_date` | Remove due date | +| `/wip` | Toggle the Work In Progress status | +| <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | +| `/remove_estimate` | Remove estimated time | +| <code>/spend <1h 30m | -1h 5m></code> | Add or subtract spent time | +| `/remove_time_spent` | Remove time spent | +| `/target_branch <Branch Name>` | Set target branch for current merge request | +| `/award :emoji:` | Toggle award for :emoji: | +| `/board_move ~column` | Move issue to column on the board | diff --git a/doc/user/project/repository/branches/img/delete_merged_branches.png b/doc/user/project/repository/branches/img/delete_merged_branches.png Binary files differnew file mode 100644 index 00000000000..1856a624f74 --- /dev/null +++ b/doc/user/project/repository/branches/img/delete_merged_branches.png diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md new file mode 100644 index 00000000000..1948627ee79 --- /dev/null +++ b/doc/user/project/repository/branches/index.md @@ -0,0 +1,17 @@ +# Branches + +## Delete merged branches + +> [Introduced][ce-6449] in GitLab 8.14. + +![Delete merged branches](img/delete_merged_branches.png) + +This feature allows merged branches to be deleted in bulk. Only branches that +have been merged and [are not protected][protected] will be deleted as part of +this operation. + +It's particularly useful to clean up old branches that were not deleting +automatically when a merge request was merged. + +[ce-6449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449 "Add button to delete all merged branches" +[protected]: ../../protected_branches.md diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 58d2fd76c61..35960ade3d4 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -27,14 +27,15 @@ with all their related data and be moved into a new GitLab instance. | GitLab version | Import/Export version | | -------- | -------- | -| 9.2.0 to current | 0.1.7 | -| 8.17.0 | 0.1.6 | -| 8.13.0 | 0.1.5 | -| 8.12.0 | 0.1.4 | -| 8.10.3 | 0.1.3 | -| 8.10.0 | 0.1.2 | -| 8.9.5 | 0.1.1 | -| 8.9.0 | 0.1.0 | +| 9.4.0 to current | 0.1.8 | +| 9.2.0 | 0.1.7 | +| 8.17.0 | 0.1.6 | +| 8.13.0 | 0.1.5 | +| 8.12.0 | 0.1.4 | +| 8.10.3 | 0.1.3 | +| 8.10.0 | 0.1.2 | +| 8.9.5 | 0.1.1 | +| 8.9.0 | 0.1.0 | > The table reflects what GitLab version we updated the Import/Export version at. > For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3) diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index 08452ca75cd..e9103a3f49c 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -1,39 +1 @@ -# GitLab slash commands - -Slash commands are textual shortcuts for common actions on issues or merge -requests that are usually done by clicking buttons or dropdowns in GitLab's UI. -You can enter these commands while creating a new issue or merge request, and -in comments. Each command should be on a separate line in order to be properly -detected and executed. The commands are removed from the issue, merge request or -comment body before it is saved and will not be visible to anyone else. - -Below is a list of all of the available commands and descriptions about what they -do. - -| Command | Action | -|:---------------------------|:-------------| -| `/close` | Close the issue or merge request | -| `/reopen` | Reopen the issue or merge request | -| `/merge` | Merge (when pipeline succeeds) | -| `/title <New title>` | Change title | -| `/assign @username` | Assign | -| `/unassign` | Remove assignee | -| `/milestone %milestone` | Set milestone | -| `/remove_milestone` | Remove milestone | -| `/label ~foo ~"bar baz"` | Add label(s) | -| `/unlabel ~foo ~"bar baz"` | Remove all or specific label(s) | -| `/relabel ~foo ~"bar baz"` | Replace all label(s) | -| `/todo` | Add a todo | -| `/done` | Mark todo as done | -| `/subscribe` | Subscribe | -| `/unsubscribe` | Unsubscribe | -| <code>/due <in 2 days | this Friday | December 31st></code> | Set due date | -| `/remove_due_date` | Remove due date | -| `/wip` | Toggle the Work In Progress status | -| <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | -| `/remove_estimate` | Remove estimated time | -| <code>/spend <1h 30m | -1h 5m></code> | Add or subtract spent time | -| `/remove_time_spent` | Remove time spent | -| `/target_branch <Branch Name>` | Set target branch for current merge request | -| `/award :emoji:` | Toggle award for :emoji: | -| `/board_move ~column` | Move issue to column on the board | +This document was moved to [user/project/quick_actions.md](quick_actions.md). diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 604c7d5cefb..54d4028a50a 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -21,7 +21,7 @@ - [Project users](add-user/add-user.md) - [Protected branches](../user/project/protected_branches.md) - [Protected tags](../user/project/protected_tags.md) -- [Slash commands](../user/project/slash_commands.md) +- [Quick Actions](../user/project/quick_actions.md) - [Sharing a project with a group](share_with_group.md) - [Share projects with other groups](share_projects_with_other_groups.md) - [Time tracking](time_tracking.md) diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md index de12994c516..bfe87bb2ceb 100644 --- a/doc/workflow/time_tracking.md +++ b/doc/workflow/time_tracking.md @@ -21,13 +21,13 @@ below. ## How to enter data -Time Tracking uses two [slash commands] that GitLab introduced with this new +Time Tracking uses two [quick actions] that GitLab introduced with this new feature: `/spend` and `/estimate`. -Slash commands can be used in the body of an issue or a merge request, but also +Quick actions can be used in the body of an issue or a merge request, but also in a comment in both an issue or a merge request. -Below is an example of how you can use those new slash commands inside a comment. +Below is an example of how you can use those new quick actions inside a comment. ![Time tracking example in a comment](time-tracking/time-tracking-example.png) @@ -70,4 +70,4 @@ The following time units are available: Default conversion rates are 1w = 5d and 1d = 8h. [landing]: https://about.gitlab.com/features/time-tracking -[slash-commands]: ../user/project/slash_commands.md +[quick actions]: ../user/project/quick_actions.md diff --git a/features/dashboard/merge_requests.feature b/features/dashboard/merge_requests.feature deleted file mode 100644 index 4a2c997d707..00000000000 --- a/features/dashboard/merge_requests.feature +++ /dev/null @@ -1,21 +0,0 @@ -@dashboard -Feature: Dashboard Merge Requests - Background: - Given I sign in as a user - And I have authored merge requests - And I have assigned merge requests - And I have other merge requests - And I visit dashboard merge requests page - - Scenario: I should see assigned merge_requests - Then I should see merge requests assigned to me - - @javascript - Scenario: I should see authored merge_requests - When I click "Authored by me" link - Then I should see merge requests authored by me - - @javascript - Scenario: I should see all merge_requests - When I click "All" link - Then I should see all merge requests diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature deleted file mode 100644 index 0b23bbb7951..00000000000 --- a/features/dashboard/todos.feature +++ /dev/null @@ -1,28 +0,0 @@ -@dashboard -Feature: Dashboard Todos - Background: - Given I sign in as a user - And I own project "Shop" - And "John Doe" is a developer of project "Shop" - And "Mary Jane" is a developer of project "Shop" - And "Mary Jane" owns private project "Enterprise" - And I am a developer of project "Enterprise" - And I have todos - And I visit dashboard todos page - - @javascript - Scenario: I mark todos as done - Then I should see todos assigned to me - And I mark the todo as done - Then I should see the todo marked as done - - @javascript - Scenario: I mark all todos as done - Then I should see todos assigned to me - And I mark all todos as done - Then I should see all todos marked as done - - @javascript - Scenario: I click on a todo row - Given I click on the todo - Then I should be directed to the corresponding page diff --git a/features/group/members.feature b/features/group/members.feature index e539f6a1273..49a44f57cbb 100644 --- a/features/group/members.feature +++ b/features/group/members.feature @@ -4,65 +4,6 @@ Feature: Group Members And "John Doe" is owner of group "Owned" And "John Doe" is guest of group "Guest" - # Leave - - @javascript - Scenario: Owner should be able to remove himself from group if he is not the last owner - Given "Mary Jane" is owner of group "Owned" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - When I click on the "Remove User From Group" button for "John Doe" - And I visit group "Owned" members page - Then I should not see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - - @javascript - Scenario: Owner should not be able to remove himself from group if he is the last owner - Given "Mary Jane" is guest of group "Owned" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - Then I should not see the "Remove User From Group" button for "John Doe" - - @javascript - Scenario: Guest should be able to remove himself from group - Given "Mary Jane" is guest of group "Guest" - When I visit group "Guest" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - When I click on the "Remove User From Group" button for "John Doe" - When I visit group "Guest" members page - Then I should not see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - - @javascript - Scenario: Guest should be able to remove himself from group even if he is the only user in the group - When I visit group "Guest" members page - Then I should see user "John Doe" in team list - When I click on the "Remove User From Group" button for "John Doe" - When I visit group "Guest" members page - Then I should not see user "John Doe" in team list - - # Remove others - - Scenario: Owner should be able to remove other users from group - Given "Mary Jane" is owner of group "Owned" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - When I click on the "Remove User From Group" button for "Mary Jane" - When I visit group "Owned" members page - Then I should see user "John Doe" in team list - Then I should not see user "Mary Jane" in team list - - Scenario: Guest should not be able to remove other users from group - Given "Mary Jane" is guest of group "Guest" - When I visit group "Guest" members page - Then I should see user "John Doe" in team list - Then I should see user "Mary Jane" in team list - Then I should not see the "Remove User From Group" button for "Mary Jane" - Scenario: Search member by name Given "Mary Jane" is guest of group "Guest" And I visit group "Guest" members page diff --git a/features/profile/notifications.feature b/features/profile/notifications.feature deleted file mode 100644 index ef8743932f5..00000000000 --- a/features/profile/notifications.feature +++ /dev/null @@ -1,15 +0,0 @@ -@profile -Feature: Profile Notifications - Background: - Given I sign in as a user - And I own project "Shop" - - Scenario: I visit notifications tab - When I visit profile notifications page - Then I should see global notifications settings - - @javascript - Scenario: I edit Project Notifications - Given I visit profile notifications page - When I select Mention setting from dropdown - Then I should see Notification saved message diff --git a/features/project/create.feature b/features/project/create.feature deleted file mode 100644 index 67336d73bf7..00000000000 --- a/features/project/create.feature +++ /dev/null @@ -1,14 +0,0 @@ -@project-create -Feature: Project Create - In order to get access to project sections - A user with ability to create a project - Should be able to create a new one - - @javascript - Scenario: User create a project - Given I sign in as a user - And I have an ssh key - When I visit new project page - And fill project form with valid data - Then I should see project page - And I should see empty project instructions diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb deleted file mode 100644 index 909ffec3646..00000000000 --- a/features/steps/dashboard/merge_requests.rb +++ /dev/null @@ -1,121 +0,0 @@ -class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include Select2Helper - - step 'I should see merge requests assigned to me' do - should_see(assigned_merge_request) - should_see(assigned_merge_request_from_fork) - should_not_see(authored_merge_request) - should_not_see(authored_merge_request_from_fork) - should_not_see(other_merge_request) - end - - step 'I should see merge requests authored by me' do - should_see(authored_merge_request) - should_see(authored_merge_request_from_fork) - should_not_see(assigned_merge_request) - should_not_see(assigned_merge_request_from_fork) - should_not_see(other_merge_request) - end - - step 'I should see all merge requests' do - should_see(authored_merge_request) - should_see(assigned_merge_request) - should_see(other_merge_request) - end - - step 'I have authored merge requests' do - authored_merge_request - authored_merge_request_from_fork - end - - step 'I have assigned merge requests' do - assigned_merge_request - assigned_merge_request_from_fork - end - - step 'I have other merge requests' do - other_merge_request - end - - step 'I click "Authored by me" link' do - find("#assignee_id").set("") - find(".js-author-search", match: :first).click - find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click - end - - step 'I click "All" link' do - find(".js-author-search").click - expect(page).to have_selector(".dropdown-menu-author li a") - find(".dropdown-menu-author li a", match: :first).click - expect(page).not_to have_selector(".dropdown-menu-author li a") - - find(".js-assignee-search").click - expect(page).to have_selector(".dropdown-menu-assignee li a") - find(".dropdown-menu-assignee li a", match: :first).click - expect(page).not_to have_selector(".dropdown-menu-assignee li a") - end - - def should_see(merge_request) - expect(page).to have_content(merge_request.title[0..10]) - end - - def should_not_see(merge_request) - expect(page).not_to have_content(merge_request.title[0..10]) - end - - def assigned_merge_request - @assigned_merge_request ||= create :merge_request, - assignee: current_user, - target_project: project, - source_project: project - end - - def authored_merge_request - @authored_merge_request ||= create :merge_request, - source_branch: 'markdown', - author: current_user, - target_project: project, - source_project: project - end - - def other_merge_request - @other_merge_request ||= create :merge_request, - source_branch: 'fix', - target_project: project, - source_project: project - end - - def authored_merge_request_from_fork - @authored_merge_request_from_fork ||= create :merge_request, - source_branch: 'feature_conflict', - author: current_user, - target_project: public_project, - source_project: forked_project - end - - def assigned_merge_request_from_fork - @assigned_merge_request_from_fork ||= create :merge_request, - source_branch: 'markdown', - assignee: current_user, - target_project: public_project, - source_project: forked_project - end - - def project - @project ||= begin - project = create(:project, :repository) - project.team << [current_user, :master] - project - end - end - - def public_project - @public_project ||= create(:project, :public, :repository) - end - - def forked_project - @forked_project ||= Projects::ForkService.new(public_project, current_user).execute - end -end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb deleted file mode 100644 index 4a33babe3bd..00000000000 --- a/features/steps/dashboard/todos.rb +++ /dev/null @@ -1,191 +0,0 @@ -class Spinach::Features::DashboardTodos < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedUser - include WaitForRequests - - step '"John Doe" is a developer of project "Shop"' do - project.team << [john_doe, :developer] - end - - step 'I am a developer of project "Enterprise"' do - enterprise.team << [current_user, :developer] - end - - step '"Mary Jane" is a developer of project "Shop"' do - project.team << [john_doe, :developer] - end - - step 'I have todos' do - create(:todo, user: current_user, project: project, author: mary_jane, target: issue, action: Todo::MENTIONED) - create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::ASSIGNED) - note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?", project: project) - create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::MENTIONED, note: note) - create(:todo, user: current_user, project: project, author: john_doe, target: merge_request, action: Todo::ASSIGNED) - end - - step 'I should see todos assigned to me' do - merge_request_reference = merge_request.to_reference(full: true) - issue_reference = issue.to_reference(full: true) - - page.within('.todos-count') { expect(page).to have_content '4' } - expect(page).to have_content 'To do 4' - expect(page).to have_content 'Done 0' - - expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title) - should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?") - should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title) - should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title) - end - - step 'I mark the todo as done' do - page.within('.todo:nth-child(1)') do - click_link 'Done' - end - - page.within('.todos-count') { expect(page).to have_content '3' } - expect(page).to have_content 'To do 3' - expect(page).to have_content 'Done 1' - should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_reversible) - end - - step 'I mark all todos as done' do - merge_request_reference = merge_request.to_reference(full: true) - issue_reference = issue.to_reference(full: true) - - find('.js-todos-mark-all').trigger('click') - - page.within('.todos-count') { expect(page).to have_content '0' } - expect(page).to have_content 'To do 0' - expect(page).to have_content 'Done 4' - expect(page).to have_content "You're all done!" - expect('.prepend-top-default').not_to have_link project.name_with_namespace - should_not_see_todo "John Doe assigned you merge request #{merge_request_reference}" - should_not_see_todo "John Doe mentioned you on issue #{issue_reference}" - should_not_see_todo "John Doe assigned you issue #{issue_reference}" - should_not_see_todo "Mary Jane mentioned you on issue #{issue_reference}" - end - - step 'I should see the todo marked as done' do - find('.todos-done a').trigger('click') - - expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_irreversible) - end - - step 'I should see all todos marked as done' do - merge_request_reference = merge_request.to_reference(full: true) - issue_reference = issue.to_reference(full: true) - - find('.todos-done a').trigger('click') - - expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, state: :done_irreversible) - should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", state: :done_irreversible) - should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, state: :done_irreversible) - should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, state: :done_irreversible) - end - - step 'I filter by "Enterprise"' do - click_button 'Project' - page.within '.dropdown-menu-project' do - click_link enterprise.name_with_namespace - end - end - - step 'I filter by "John Doe"' do - click_button 'Author' - page.within '.dropdown-menu-author' do - click_link john_doe.username - end - end - - step 'I filter by "Issue"' do - click_button 'Type' - page.within '.dropdown-menu-type' do - click_link 'Issue' - end - end - - step 'I filter by "Mentioned"' do - click_button 'Action' - page.within '.dropdown-menu-action' do - click_link 'Mentioned' - end - end - - step 'I should not see todos' do - expect(page).to have_content "You're all done!" - end - - step 'I should not see todos related to "Mary Jane" in the list' do - should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference(full: true)}" - end - - step 'I should not see todos related to "Merge Requests" in the list' do - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}" - end - - step 'I should not see todos related to "Assignments" in the list' do - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}" - should_not_see_todo "John Doe assigned you issue #{issue.to_reference(full: true)}" - end - - step 'I click on the todo' do - find('.todo:nth-child(1)').click - end - - step 'I should be directed to the corresponding page' do - page.should have_css('.identifier', text: 'Merge request !1') - # Merge request page loads and issues a number of Ajax requests - wait_for_requests - end - - def should_see_todo(position, title, body, state: :pending) - page.within(".todo:nth-child(#{position})") do - expect(page).to have_content title - expect(page).to have_content body - - if state == :pending - expect(page).to have_link 'Done' - elsif state == :done_reversible - expect(page).to have_link 'Undo' - elsif state == :done_irreversible - expect(page).not_to have_link 'Undo' - expect(page).not_to have_link 'Done' - else - raise 'Invalid state given, valid states: :pending, :done_reversible, :done_irreversible' - end - end - end - - def should_not_see_todo(title) - expect(page).not_to have_visible_content title - end - - def have_visible_content(text) - have_css('*', text: text, visible: true) - end - - def john_doe - @john_doe ||= user_exists("John Doe", { username: "john_doe" }) - end - - def mary_jane - @mary_jane ||= user_exists("Mary Jane", { username: "mary_jane" }) - end - - def enterprise - @enterprise ||= Project.find_by(name: 'Enterprise') - end - - def issue - @issue ||= create(:issue, assignees: [current_user], project: project) - end - - def merge_request - @merge_request ||= create(:merge_request, assignee: current_user, source_project: project) - end -end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 25bb374b868..0aedc422563 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -5,7 +5,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedUser step 'I should see group "Owned"' do - expect(page).to have_content '@owned' + expect(page).to have_content 'Owned' end step 'I am a signed out user' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 80aa3a047a0..9ed4f8ea1f9 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -369,7 +369,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content 'Permalink' expect(page).not_to have_content 'Edit' expect(page).not_to have_content 'Blame' - expect(page).not_to have_content 'Annotate' expect(page).to have_content 'Delete' expect(page).to have_content 'Replace' end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index f0e751b820a..8a5b4112ffe 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -112,10 +112,6 @@ module SharedPaths visit dashboard_groups_path end - step 'I visit dashboard todos page' do - visit dashboard_todos_path - end - step 'I should be redirected to the dashboard groups page' do expect(current_path).to eq dashboard_groups_path end diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index a5c9f0b509c..c9b5f58c557 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -68,8 +68,8 @@ module API delete ":id/access_requests/:user_id" do source = find_source(source_type, params[:id]) - ::Members::DestroyService.new(source, current_user, params). - execute(:requesters) + ::Members::DestroyService.new(source, current_user, params) + .execute(:requesters) end end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index f35084a582a..3d816f8771d 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -102,8 +102,8 @@ module API post ":id/repository/branches" do authorize_push_project - result = CreateBranchService.new(user_project, current_user). - execute(params[:branch], params[:ref]) + result = CreateBranchService.new(user_project, current_user) + .execute(params[:branch], params[:ref]) if result[:status] == :success present result[:branch], @@ -121,8 +121,8 @@ module API delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do authorize_push_project - result = DeleteBranchService.new(user_project, current_user). - execute(params[:branch]) + result = DeleteBranchService.new(user_project, current_user) + .execute(params[:branch]) if result[:status] != :success render_api_error!(result[:message], result[:return_code]) diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 10f2d5ef6a3..485b680cd5f 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -108,6 +108,9 @@ module API render_api_error!('invalid state', 400) end + MergeRequest.where(source_project: @project, source_branch: ref) + .update_all(head_pipeline_id: pipeline) if pipeline.latest? + present status, with: Entities::CommitStatus rescue StateMachines::InvalidTransition => e render_api_error!(e.message, 400) diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 7cdee8aced7..d5c2f3d5094 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -86,7 +86,7 @@ module API at_least_one_of :title, :can_push end put ":id/deploy_keys/:key_id" do - key = user_project.deploy_keys.find(params.delete(:key_id)) + key = DeployKey.find(params.delete(:key_id)) authorize!(:update_deploy_key, key) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a836df3dc81..aa91451c9f4 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -43,11 +43,14 @@ module API expose :external end - class UserWithPrivateDetails < UserPublic - expose :private_token + class UserWithAdmin < UserPublic expose :admin?, as: :is_admin end + class UserWithPrivateDetails < UserWithAdmin + expose :private_token + end + class Email < Grape::Entity expose :id, :email end @@ -115,6 +118,7 @@ module API expose :only_allow_merge_if_pipeline_succeeds expose :request_access_enabled expose :only_allow_merge_if_all_discussions_are_resolved + expose :printing_merge_request_link_enabled expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics end @@ -480,9 +484,9 @@ module API expose :job_events # Expose serialized properties expose :properties do |service, options| - field_names = service.fields. - select { |field| options[:include_passwords] || field[:type] != 'password' }. - map { |field| field[:name] } + field_names = service.fields + .select { |field| options[:include_passwords] || field[:type] != 'password' } + .map { |field| field[:name] } service.properties.slice(*field_names) end end @@ -603,6 +607,9 @@ module API expose :plantuml_url expose :terminal_max_session_time expose :polling_interval_multiplier + expose :help_page_hide_commercial_content + expose :help_page_text + expose :help_page_support_url end class Release < Grape::Entity @@ -804,7 +811,11 @@ module API end class Image < Grape::Entity - expose :name + expose :name, :entrypoint + end + + class Service < Image + expose :alias, :command end class Artifacts < Grape::Entity @@ -848,7 +859,7 @@ module API expose :variables expose :steps, using: Step expose :image, using: Image - expose :services, using: Image + expose :services, using: Service expose :artifacts, using: Artifacts expose :cache, using: Cache expose :credentials, using: Credentials diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index d3732d67622..5e9cf5e68b1 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -10,6 +10,10 @@ module API set_project unless defined?(@project) @project end + + def redirected_path + @redirected_path + end def ssh_authentication_abilities [ @@ -38,8 +42,9 @@ module API def set_project if params[:gl_repository] @project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository]) + @redirected_path = nil else - @project, @wiki = Gitlab::RepoPath.parse(params[:project]) + @project, @wiki, @redirected_path = Gitlab::RepoPath.parse(params[:project]) end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 38631953014..f1c79970ba4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -34,7 +34,7 @@ module API access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess access_checker = access_checker_klass - .new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities) + .new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, redirected_path: redirected_path) begin access_checker.check(params[:action], params[:changes]) @@ -71,11 +71,16 @@ module API end # - # Discover user by ssh key + # Discover user by ssh key or user id # get "/discover" do - key = Key.find(params[:key_id]) - present key.user, with: Entities::UserSafe + if params[:key_id] + key = Key.find(params[:key_id]) + user = key.user + elsif params[:user_id] + user = User.find_by(id: params[:user_id]) + end + present user, with: Entities::UserSafe end get "/check" do @@ -86,8 +91,16 @@ module API } end + get "/broadcast_messages" do + if messages = BroadcastMessage.current + present messages, with: Entities::BroadcastMessage + else + [] + end + end + get "/broadcast_message" do - if message = BroadcastMessage.current + if message = BroadcastMessage.current.last present message, with: Entities::BroadcastMessage else {} @@ -119,8 +132,11 @@ module API return { success: false, message: 'Two-factor authentication is not enabled for this user' } end - codes = user.generate_otp_backup_codes! - user.save! + codes = nil + + ::Users::UpdateService.new(user).execute! do |user| + codes = user.generate_otp_backup_codes! + end { success: true, recovery_codes: codes } end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 78db960ae28..09dca0dff8b 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -27,6 +27,8 @@ module API optional :milestone, type: String, desc: 'Return issues for a specific milestone' optional :iids, type: Array[Integer], desc: 'The IID array of issues' optional :search, type: String, desc: 'Search issues for text present in the title or description' + optional :created_after, type: DateTime, desc: 'Return issues created after the specified time' + optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' use :pagination end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 710deba5ae3..1118fc7465b 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -72,6 +72,8 @@ module API optional :iids, type: Array[Integer], desc: 'The IID array of merge requests' optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time' + optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time' use :pagination end get ":id/merge_requests" do @@ -97,7 +99,7 @@ module API authorize! :create_merge_request, user_project mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index a3ea619a2fb..3541d3c95fb 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -117,7 +117,7 @@ module API finder_params = { project_id: user_project.id, milestone_title: milestone.title, - sort: 'position_asc' + sort: 'label_priority' } issues = IssuesFinder.new(current_user, finder_params).execute @@ -140,7 +140,7 @@ module API finder_params = { project_id: user_project.id, milestone_title: milestone.title, - sort: 'position_asc' + sort: 'label_priority' } merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute diff --git a/lib/api/notes.rb b/lib/api/notes.rb index e281e3230fd..01ca62b593f 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -33,8 +33,8 @@ module API # paginate() only works with a relation. This could lead to a # mismatch between the pagination headers info and the actual notes # array returned, but this is really a edge-case. - paginate(noteable.notes). - reject { |n| n.cross_reference_not_visible_for?(current_user) } + paginate(noteable.notes) + .reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: Entities::Note else not_found!("Notes") diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index 992ea5dc24d..5d113c94b22 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -34,7 +34,10 @@ module API notification_setting.transaction do new_notification_email = params.delete(:notification_email) - current_user.update(notification_email: new_notification_email) if new_notification_email + if new_notification_email + ::Users::UpdateService.new(current_user, notification_email: new_notification_email).execute + end + notification_setting.update(declared_params(include_missing: false)) end rescue ArgumentError => e # catch level enum error diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 50d34e8a738..c5df45b7902 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -23,6 +23,7 @@ module API optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' optional :tag_list, type: Array[String], desc: 'The list of tags for a project' optional :avatar, type: File, desc: 'Avatar image for project' + optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' end params :optional_params do @@ -218,6 +219,7 @@ module API :only_allow_merge_if_all_discussions_are_resolved, :only_allow_merge_if_pipeline_succeeds, :path, + :printing_merge_request_link_enabled, :public_builds, :request_access_enabled, :shared_runners_enabled, diff --git a/lib/api/services.rb b/lib/api/services.rb index 47bd9940f77..7488f95a9b7 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -685,7 +685,7 @@ module API trigger_services.each do |service_slug, settings| helpers do - def chat_command_service(project, service_slug, params) + def slash_command_service(project, service_slug, params) project.services.active.where(template: false).find do |service| service.try(:token) == params[:token] && service.to_param == service_slug.underscore end @@ -710,7 +710,7 @@ module API # This is not accurate, but done to prevent leakage of the project names not_found!('Service') unless project - service = chat_command_service(project, service_slug, params) + service = slash_command_service(project, service_slug, params) result = service.try(:trigger, params) if result diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 25027c3b114..d598f9a62a2 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -39,7 +39,9 @@ module API :email_author_in_body, :enabled_git_access_protocol, :gravatar_enabled, + :help_page_hide_commercial_content, :help_page_text, + :help_page_support_url, :home_page_url, :housekeeping_enabled, :html_emails_enabled, @@ -101,7 +103,9 @@ module API optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out' optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application' + optional :help_page_hide_commercial_content, type: Boolean, desc: 'Hide marketing-related entries from help' optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' + optional :help_page_support_url, type: String, desc: 'Alternate support URL for help page' optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects' given shared_runners_enabled: ->(val) { val } do requires :shared_runners_text, type: String, desc: 'Shared runners text ' diff --git a/lib/api/tags.rb b/lib/api/tags.rb index c7b1efe0bfa..633a858f8c7 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -44,8 +44,8 @@ module API post ':id/repository/tags' do authorize_push_project - result = ::Tags::CreateService.new(user_project, current_user). - execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) + result = ::Tags::CreateService.new(user_project, current_user) + .execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) if result[:status] == :success present result[:tag], @@ -63,8 +63,8 @@ module API delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do authorize_push_project - result = ::Tags::DestroyService.new(user_project, current_user). - execute(params[:tag_name]) + result = ::Tags::DestroyService.new(user_project, current_user) + .execute(params[:tag_name]) if result[:status] != :success render_api_error!(result[:message], result[:return_code]) @@ -81,8 +81,8 @@ module API post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do authorize_push_project - result = CreateReleaseService.new(user_project, current_user). - execute(params[:tag_name], params[:description]) + result = CreateReleaseService.new(user_project, current_user) + .execute(params[:tag_name], params[:description]) if result[:status] == :success present result[:release], with: Entities::Release @@ -101,8 +101,8 @@ module API put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do authorize_push_project - result = UpdateReleaseService.new(user_project, current_user). - execute(params[:tag_name], params[:description]) + result = UpdateReleaseService.new(user_project, current_user) + .execute(params[:tag_name], params[:description]) if result[:status] == :success present result[:release], with: Entities::Release diff --git a/lib/api/users.rb b/lib/api/users.rb index dda64715ee1..f9555842daf 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -29,6 +29,7 @@ module API optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' + optional :avatar, type: File, desc: 'Avatar image for user' all_or_none_of :extern_uid, :provider end end @@ -58,7 +59,7 @@ module API users = UsersFinder.new(current_user, params).execute - entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic + entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic present paginate(users), with: entity end @@ -97,18 +98,18 @@ module API authenticated_as_admin! params = declared_params(include_missing: false) - user = ::Users::CreateService.new(current_user, params).execute + user = ::Users::CreateService.new(current_user, params).execute(skip_authorization: true) if user.persisted? present user, with: Entities::UserPublic else - conflict!('Email has already been taken') if User. - where(email: user.email). - count > 0 + conflict!('Email has already been taken') if User + .where(email: user.email) + .count > 0 - conflict!('Username has already been taken') if User. - where(username: user.username). - count > 0 + conflict!('Username has already been taken') if User + .where(username: user.username) + .count > 0 render_validation_error!(user) end @@ -132,12 +133,12 @@ module API not_found!('User') unless user conflict!('Email has already been taken') if params[:email] && - User.where(email: params[:email]). - where.not(id: user.id).count > 0 + User.where(email: params[:email]) + .where.not(id: user.id).count > 0 conflict!('Username has already been taken') if params[:username] && - User.where(username: params[:username]). - where.not(id: user.id).count > 0 + User.where(username: params[:username]) + .where.not(id: user.id).count > 0 user_params = declared_params(include_missing: false) identity_attrs = user_params.slice(:provider, :extern_uid) @@ -155,7 +156,9 @@ module API user_params[:password_expires_at] = Time.now if user_params[:password].present? - if user.update_attributes(user_params.except(:extern_uid, :provider)) + result = ::Users::UpdateService.new(user, user_params.except(:extern_uid, :provider)).execute + + if result[:status] == :success present user, with: Entities::UserPublic else render_validation_error!(user) @@ -233,9 +236,9 @@ module API user = User.find_by(id: params.delete(:id)) not_found!('User') unless user - email = user.emails.new(declared_params(include_missing: false)) + email = Emails::CreateService.new(user, declared_params(include_missing: false)).execute - if email.save + if email.errors.blank? NotificationService.new.new_email(email) present email, with: Entities::Email else @@ -273,8 +276,7 @@ module API email = user.emails.find_by(id: params[:email_id]) not_found!('Email') unless email - email.destroy - user.update_secondary_emails! + Emails::DestroyService.new(user, email: email.email).execute end desc 'Delete a user. Available only for admins.' do @@ -486,9 +488,9 @@ module API requires :email, type: String, desc: 'The new email' end post "emails" do - email = current_user.emails.new(declared_params) + email = Emails::CreateService.new(current_user, declared_params).execute - if email.save + if email.errors.blank? NotificationService.new.new_email(email) present email, with: Entities::Email else @@ -504,8 +506,7 @@ module API email = current_user.emails.find_by(id: params[:email_id]) not_found!('Email') unless email - email.destroy - current_user.update_secondary_emails! + Emails::DestroyService.new(current_user, email: email.email).execute end desc 'Get a list of user activities' @@ -516,9 +517,9 @@ module API get "activities" do authenticated_as_admin! - activities = User. - where(User.arel_table[:last_activity_on].gteq(params[:from])). - reorder(last_activity_on: :asc) + activities = User + .where(User.arel_table[:last_activity_on].gteq(params[:from])) + .reorder(last_activity_on: :asc) present paginate(activities), with: Entities::UserActivity end diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb index 0a877b960f6..81b13249892 100644 --- a/lib/api/v3/branches.rb +++ b/lib/api/v3/branches.rb @@ -26,8 +26,8 @@ module API delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do authorize_push_project - result = DeleteBranchService.new(user_project, current_user). - execute(params[:branch]) + result = DeleteBranchService.new(user_project, current_user) + .execute(params[:branch]) if result[:status] == :success status(200) @@ -55,8 +55,8 @@ module API end post ":id/repository/branches" do authorize_push_project - result = CreateBranchService.new(user_project, current_user). - execute(params[:branch_name], params[:ref]) + result = CreateBranchService.new(user_project, current_user) + .execute(params[:branch_name], params[:ref]) if result[:status] == :success present result[:branch], diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 7c5065dee90..c848f52723b 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -245,9 +245,9 @@ module API expose :job_events, as: :build_events # Expose serialized properties expose :properties do |service, options| - field_names = service.fields. - select { |field| options[:include_passwords] || field[:type] != 'password' }. - map { |field| field[:name] } + field_names = service.fields + .select { |field| options[:include_passwords] || field[:type] != 'password' } + .map { |field| field[:name] } service.properties.slice(*field_names) end end diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb index d9e76560d03..4e63aa01c1a 100644 --- a/lib/api/v3/helpers.rb +++ b/lib/api/v3/helpers.rb @@ -38,7 +38,10 @@ module API projects = projects.where(visibility_level: Gitlab::VisibilityLevel.level_value(params[:visibility])) end - projects = projects.where(archived: params[:archived]) + unless params[:archived].nil? + projects = projects.where(archived: to_boolean(params[:archived])) + end + projects.reorder(params[:order_by] => params[:sort]) end end diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb index 009ec5c6bbd..23fe95e42e4 100644 --- a/lib/api/v3/notes.rb +++ b/lib/api/v3/notes.rb @@ -34,8 +34,8 @@ module API # paginate() only works with a relation. This could lead to a # mismatch between the pagination headers info and the actual notes # array returned, but this is really a edge-case. - paginate(noteable.notes). - reject { |n| n.cross_reference_not_visible_for?(current_user) } + paginate(noteable.notes) + .reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: ::API::V3::Entities::Note else not_found!("Notes") diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index 20976b9dd08..eb090453b48 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -69,7 +69,7 @@ module API end params :filter_params do - optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' + optional :archived, type: Boolean, default: nil, desc: 'Limit by archived status' optional :visibility, type: String, values: %w[public internal private], desc: 'Limit by visibility' optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index 118c6df6549..2d13d6fabfd 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -608,7 +608,7 @@ module API trigger_services.each do |service_slug, settings| helpers do - def chat_command_service(project, service_slug, params) + def slash_command_service(project, service_slug, params) project.services.active.where(template: false).find do |service| service.try(:token) == params[:token] && service.to_param == service_slug.underscore end @@ -633,7 +633,7 @@ module API # This is not accurate, but done to prevent leakage of the project names not_found!('Service') unless project - service = chat_command_service(project, service_slug, params) + service = slash_command_service(project, service_slug, params) result = service.try(:trigger, params) if result diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb index c2541de2f50..7e5875cd030 100644 --- a/lib/api/v3/tags.rb +++ b/lib/api/v3/tags.rb @@ -22,8 +22,8 @@ module API delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do authorize_push_project - result = ::Tags::DestroyService.new(user_project, current_user). - execute(params[:tag_name]) + result = ::Tags::DestroyService.new(user_project, current_user) + .execute(params[:tag_name]) if result[:status] == :success status(200) diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index f4cda3b2eba..37020019e07 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -50,13 +50,13 @@ module API if user.persisted? present user, with: ::API::Entities::UserPublic else - conflict!('Email has already been taken') if User. - where(email: user.email). - count > 0 + conflict!('Email has already been taken') if User + .where(email: user.email) + .count > 0 - conflict!('Username has already been taken') if User. - where(username: user.username). - count > 0 + conflict!('Username has already been taken') if User + .where(username: user.username) + .count > 0 render_validation_error!(user) end @@ -137,11 +137,11 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user - events = user.events. - merge(ProjectsFinder.new(current_user: current_user).execute). - references(:project). - with_associations. - recent + events = user.events + .merge(ProjectsFinder.new(current_user: current_user).execute) + .references(:project) + .with_associations + .recent present paginate(events), with: ::API::V3::Entities::Event end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 381c4ef50b0..10374995497 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -45,7 +45,7 @@ module API optional :protected, type: String, desc: 'Whether the variable is protected' end post ':id/variables' do - variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h) + variable = user_project.variables.create(declared_params(include_missing: false)) if variable.valid? present variable, with: Entities::Variable diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb index 8e3b0c4db79..7e6357f8a00 100644 --- a/lib/banzai/reference_extractor.rb +++ b/lib/banzai/reference_extractor.rb @@ -10,8 +10,8 @@ module Banzai end def references(type, project, current_user = nil) - processor = Banzai::ReferenceParser[type]. - new(project, current_user) + processor = Banzai::ReferenceParser[type] + .new(project, current_user) processor.process(html_documents) end diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index 1e2536231d8..279fca8d043 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -171,7 +171,7 @@ module Banzai collection.where(id: to_query).each { |row| cache[row.id] = row } end - cache.values_at(*ids) + cache.values_at(*ids).compact else collection.where(id: ids) end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index 89ec715ddf6..9fd4bd68d43 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -9,8 +9,8 @@ module Banzai issues = issues_for_nodes(nodes) - readable_issues = Ability. - issues_readable_by_user(issues.values, user).to_set + readable_issues = Ability + .issues_readable_by_user(issues.values, user).to_set nodes.select do |node| readable_issues.include?(issues[node]) diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index 3efbd2fd631..4d336068861 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -99,8 +99,8 @@ module Banzai def find_users_for_projects(ids) return [] if ids.empty? - collection_objects_for_ids(Project, ids). - flat_map { |p| p.team.members.to_a } + collection_objects_for_ids(Project, ids) + .flat_map { |p| p.team.members.to_a } end def can_read_reference?(user, ref_project, node) diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index 792ff628b09..6b82b2b4f13 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -45,7 +45,21 @@ module Ci expose :artifacts_expire_at, if: ->(build, _) { build.artifacts? } expose :options do |model| - model.options + # This part ensures that output of old API is still the same after adding support + # for extended docker configuration options, used by new API + # + # I'm leaving this here, not in the model, because it should be removed at the same time + # when old API will be removed (planned for August 2017). + model.options.dup.tap do |options| + options[:image] = options[:image][:name] if options[:image].is_a?(Hash) + options[:services].map! do |service| + if service.is_a?(Hash) + service[:name] + else + service + end + end + end end expose :timeout do |model| diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 3decc3b1a26..872e418c788 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -2,10 +2,10 @@ module Ci module Charts module DailyInterval def grouped_count(query) - query. - group("DATE(#{Ci::Build.table_name}.created_at)"). - count(:created_at). - transform_keys { |date| date.strftime(@format) } + query + .group("DATE(#{Ci::Pipeline.table_name}.created_at)") + .count(:created_at) + .transform_keys { |date| date.strftime(@format) } end def interval_step @@ -16,14 +16,14 @@ module Ci module MonthlyInterval def grouped_count(query) if Gitlab::Database.postgresql? - query. - group("to_char(#{Ci::Build.table_name}.created_at, '01 Month YYYY')"). - count(:created_at). - transform_keys(&:squish) + query + .group("to_char(#{Ci::Pipeline.table_name}.created_at, '01 Month YYYY')") + .count(:created_at) + .transform_keys(&:squish) else - query. - group("DATE_FORMAT(#{Ci::Build.table_name}.created_at, '01 %M %Y')"). - count(:created_at) + query + .group("DATE_FORMAT(#{Ci::Pipeline.table_name}.created_at, '01 %M %Y')") + .count(:created_at) end end @@ -33,21 +33,21 @@ module Ci end class Chart - attr_reader :labels, :total, :success, :project, :build_times + attr_reader :labels, :total, :success, :project, :pipeline_times def initialize(project) @labels = [] @total = [] @success = [] - @build_times = [] + @pipeline_times = [] @project = project collect end def collect - query = project.builds. - where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", @to, @from) + query = project.pipelines + .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) totals_count = grouped_count(query) success_count = grouped_count(query.success) @@ -101,14 +101,14 @@ module Ci end end - class BuildTime < Chart + class PipelineTime < Chart def collect commits = project.pipelines.last(30) commits.each do |commit| @labels << commit.short_sha duration = commit.duration || 0 - @build_times << (duration / 60) + @pipeline_times << (duration / 60) end end end diff --git a/lib/feature.rb b/lib/feature.rb index 5650a1c1334..d3d972564af 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -39,8 +39,6 @@ class Feature get(key).disable end - private - def flipper @flipper ||= begin adapter = Flipper::Adapters::ActiveRecord.new( diff --git a/lib/github/import.rb b/lib/github/import.rb index b20614b3060..ff5d7db2705 100644 --- a/lib/github/import.rb +++ b/lib/github/import.rb @@ -172,7 +172,7 @@ module Github next unless merge_request.new_record? && pull_request.valid? begin - restore_branches(pull_request) + pull_request.restore_branches! author_id = user_id(pull_request.author, project.creator_id) description = format_description(pull_request.description, pull_request.author) @@ -208,7 +208,7 @@ module Github rescue => e error(:pull_request, pull_request.url, e.message) ensure - clean_up_restored_branches(pull_request) + pull_request.remove_restored_branches! end end @@ -325,32 +325,6 @@ module Github end end - def restore_branches(pull_request) - restore_source_branch(pull_request) unless pull_request.source_branch_exists? - restore_target_branch(pull_request) unless pull_request.target_branch_exists? - end - - def restore_source_branch(pull_request) - repository.create_branch(pull_request.source_branch_name, pull_request.source_branch_sha) - end - - def restore_target_branch(pull_request) - repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha) - end - - def remove_branch(name) - repository.delete_branch(name) - rescue Rugged::ReferenceError - errors << { type: :branch, url: nil, error: "Could not clean up restored branch: #{name}" } - end - - def clean_up_restored_branches(pull_request) - return if pull_request.opened? - - remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists? - remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists? - end - def label_ids(labels) labels.map { |attrs| cached[:label_ids][attrs.fetch('name')] }.compact end diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb index d1dac6944f0..c6fa928d565 100644 --- a/lib/github/representation/branch.rb +++ b/lib/github/representation/branch.rb @@ -26,13 +26,25 @@ module Github end def exists? - branch_exists? && commit_exists? + @exists ||= branch_exists? && commit_exists? end def valid? sha.present? && ref.present? end + def restore!(name) + repository.create_branch(name, sha) + rescue Gitlab::Git::Repository::InvalidRef => e + Rails.logger.error("#{self.class.name}: Could not restore branch #{name}: #{e}") + end + + def remove!(name) + repository.delete_branch(name) + rescue Rugged::ReferenceError => e + Rails.logger.error("#{self.class.name}: Could not remove branch #{name}: #{e}") + end + private def branch_exists? diff --git a/lib/github/representation/pull_request.rb b/lib/github/representation/pull_request.rb index ac9c8283b4b..55461097e8a 100644 --- a/lib/github/representation/pull_request.rb +++ b/lib/github/representation/pull_request.rb @@ -1,8 +1,6 @@ module Github module Representation class PullRequest < Representation::Issuable - attr_reader :project - delegate :user, :repo, :ref, :sha, to: :source_branch, prefix: true delegate :user, :exists?, :repo, :ref, :sha, :short_sha, to: :target_branch, prefix: true @@ -10,10 +8,6 @@ module Github project end - def source_branch_exists? - !cross_project? && source_branch.exists? - end - def source_branch_name @source_branch_name ||= if cross_project? || !source_branch_exists? @@ -23,6 +17,12 @@ module Github end end + def source_branch_exists? + return @source_branch_exists if defined?(@source_branch_exists) + + @source_branch_exists = !cross_project? && source_branch.exists? + end + def target_project project end @@ -31,6 +31,10 @@ module Github @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed end + def target_branch_exists? + @target_branch_exists ||= target_branch.exists? + end + def state return 'merged' if raw['state'] == 'closed' && raw['merged_at'].present? return 'closed' if raw['state'] == 'closed' @@ -46,6 +50,18 @@ module Github source_branch.valid? && target_branch.valid? end + def restore_branches! + restore_source_branch! + restore_target_branch! + end + + def remove_restored_branches! + return if opened? + + remove_source_branch! + remove_target_branch! + end + private def project @@ -73,6 +89,32 @@ module Github source_branch_repo.id != target_branch_repo.id end + + def restore_source_branch! + return if source_branch_exists? + + source_branch.restore!(source_branch_name) + end + + def restore_target_branch! + return if target_branch_exists? + + target_branch.restore!(target_branch_name) + end + + def remove_source_branch! + # We should remove the source/target branches only if they were + # restored. Otherwise, we'll remove branches like 'master' that + # target_branch_exists? returns true. In other words, we need + # to clean up only the restored branches that (source|target)_branch_exists? + # returns false for the first time it has been called, because of + # this that is important to memoize these values. + source_branch.remove!(source_branch_name) unless source_branch_exists? + end + + def remove_target_branch! + target_branch.remove!(target_branch_name) unless target_branch_exists? + end end end end diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index 914a3b72abd..d95ecd7b291 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -5,8 +5,8 @@ module Gitlab # # steal_class - The name of the class for which to steal jobs. def self.steal(steal_class) - queue = Sidekiq::Queue. - new(BackgroundMigrationWorker.sidekiq_options['queue']) + queue = Sidekiq::Queue + .new(BackgroundMigrationWorker.sidekiq_options['queue']) queue.each do |job| migration_class, migration_args = job.args diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb index 4fc9a075edc..9c2e09943b0 100644 --- a/lib/gitlab/cache/ci/project_pipeline_status.rb +++ b/lib/gitlab/cache/ci/project_pipeline_status.rb @@ -50,8 +50,8 @@ module Gitlab ref: pipeline.ref } - new(pipeline.project, pipeline_info: pipeline_info). - store_in_cache_if_needed + new(pipeline.project, pipeline_info: pipeline_info) + .store_in_cache_if_needed end def initialize(project, pipeline_info: {}, loaded_from_cache: nil) diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index c62aeb60fa9..b88b2e36d53 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -2,7 +2,7 @@ module Gitlab module Ci module Build class Image - attr_reader :name + attr_reader :alias, :command, :entrypoint, :name class << self def from_image(job) @@ -21,7 +21,14 @@ module Gitlab end def initialize(image) - @name = image + if image.is_a?(String) + @name = image + elsif image.is_a?(Hash) + @alias = image[:alias] + @command = image[:command] + @entrypoint = image[:entrypoint] + @name = image[:name] + end end def valid? diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index b5050257688..897dcff8012 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -8,8 +8,36 @@ module Gitlab class Image < Node include Validatable + ALLOWED_KEYS = %i[name entrypoint].freeze + validations do - validates :config, type: String + validates :config, hash_or_string: true + validates :config, allowed_keys: ALLOWED_KEYS + + validates :name, type: String, presence: true + validates :entrypoint, type: String, allow_nil: true + end + + def hash? + @config.is_a?(Hash) + end + + def string? + @config.is_a?(String) + end + + def name + value[:name] + end + + def entrypoint + value[:entrypoint] + end + + def value + return { name: @config } if string? + return @config if hash? + {} end end end diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb new file mode 100644 index 00000000000..b52faf48b58 --- /dev/null +++ b/lib/gitlab/ci/config/entry/service.rb @@ -0,0 +1,34 @@ +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a configuration of Docker service. + # + class Service < Image + include Validatable + + ALLOWED_KEYS = %i[name entrypoint command alias].freeze + + validations do + validates :config, hash_or_string: true + validates :config, allowed_keys: ALLOWED_KEYS + + validates :name, type: String, presence: true + validates :entrypoint, type: String, allow_nil: true + validates :command, type: String, allow_nil: true + validates :alias, type: String, allow_nil: true + end + + def alias + value[:alias] + end + + def command + value[:command] + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index 84f8ab780f5..0066894e069 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -9,7 +9,30 @@ module Gitlab include Validatable validations do - validates :config, array_of_strings: true + validates :config, type: Array + end + + def compose!(deps = nil) + super do + @entries = [] + @config.each do |config| + @entries << Entry::Factory.new(Entry::Service) + .value(config || {}) + .create! + end + + @entries.each do |entry| + entry.compose!(deps) + end + end + end + + def value + @entries.map(&:value) + end + + def descendants + @entries end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index bd7428b1272..b2ca3c881e4 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -44,6 +44,14 @@ module Gitlab end end + class HashOrStringValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || value.is_a?(String) + record.errors.add(attribute, 'should be a hash or a string') + end + end + end + class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/lib/gitlab/ci/pipeline_duration.rb b/lib/gitlab/ci/pipeline_duration.rb index a210e76acaa..3208cc2bef6 100644 --- a/lib/gitlab/ci/pipeline_duration.rb +++ b/lib/gitlab/ci/pipeline_duration.rb @@ -87,8 +87,8 @@ module Gitlab def from_pipeline(pipeline) status = %w[success failed running canceled] - builds = pipeline.builds.latest. - where(status: status).where.not(started_at: nil).order(:started_at) + builds = pipeline.builds.latest + .where(status: status).where.not(started_at: nil).order(:started_at) from_builds(builds) end diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb index 4969a350862..9307545b5b1 100644 --- a/lib/gitlab/ci/status/external/common.rb +++ b/lib/gitlab/ci/status/external/common.rb @@ -3,6 +3,10 @@ module Gitlab module Status module External module Common + def label + subject.description + end + def has_details? subject.target_url.present? && can?(user, :read_commit_status, subject) diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index 6e73361cad1..1611eba31da 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -16,9 +16,9 @@ module Gitlab project = merge_request.source_project new(merge_request, project).tap do |file_collection| - project. - repository. - with_repo_branch_commit(merge_request.target_project.repository, merge_request.target_branch) do + project + .repository + .with_repo_branch_commit(merge_request.target_project.repository, merge_request.target_branch) do yield file_collection end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 060e013183f..bf557103cfd 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -16,14 +16,14 @@ module Gitlab # Can't use Event.contributions here because we need to check 3 different # project_features for the (currently) 3 different contribution types date_from = 1.year.ago - repo_events = event_counts(date_from, :repository). - having(action: Event::PUSHED) - issue_events = event_counts(date_from, :issues). - having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue") - mr_events = event_counts(date_from, :merge_requests). - having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest") - note_events = event_counts(date_from, :merge_requests). - having(action: [Event::COMMENTED], target_type: "Note") + repo_events = event_counts(date_from, :repository) + .having(action: Event::PUSHED) + issue_events = event_counts(date_from, :issues) + .having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue") + mr_events = event_counts(date_from, :merge_requests) + .having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest") + note_events = event_counts(date_from, :merge_requests) + .having(action: [Event::COMMENTED], target_type: "Note") union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events, note_events]) events = Event.find_by_sql(union.to_sql).map(&:attributes) @@ -34,9 +34,9 @@ module Gitlab end def events_by_date(date) - events = Event.contributions.where(author_id: contributor.id). - where(created_at: date.beginning_of_day..date.end_of_day). - where(project_id: projects) + events = Event.contributions.where(author_id: contributor.id) + .where(created_at: date.beginning_of_day..date.end_of_day) + .where(project_id: projects) # Use visible_to_user? instead of the complicated logic in activity_dates # because we're only viewing the events for a single day. @@ -60,20 +60,20 @@ module Gitlab # use IN(project_ids...) instead. It's the intersection of two users so # the list will be (relatively) short @contributed_project_ids ||= projects.uniq.pluck(:id) - authed_projects = Project.where(id: @contributed_project_ids). - with_feature_available_for_user(feature, current_user). - reorder(nil). - select(:id) + authed_projects = Project.where(id: @contributed_project_ids) + .with_feature_available_for_user(feature, current_user) + .reorder(nil) + .select(:id) - conditions = t[:created_at].gteq(date_from.beginning_of_day). - and(t[:created_at].lteq(Date.today.end_of_day)). - and(t[:author_id].eq(contributor.id)) + conditions = t[:created_at].gteq(date_from.beginning_of_day) + .and(t[:created_at].lteq(Date.today.end_of_day)) + .and(t[:author_id].eq(contributor.id)) - Event.reorder(nil). - select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount'). - group(t[:project_id], t[:target_type], t[:action], 'date(created_at)'). - where(conditions). - having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) + Event.reorder(nil) + .select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount') + .group(t[:project_id], t[:target_type], t[:action], 'date(created_at)') + .where(conditions) + .having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 48735fd197d..818b3d9c46b 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -10,43 +10,49 @@ module Gitlab delegate :sidekiq_throttling_enabled?, to: :current_application_settings - def fake_application_settings - OpenStruct.new(::ApplicationSetting.defaults) + def fake_application_settings(defaults = ::ApplicationSetting.defaults) + FakeApplicationSettings.new(defaults) end private def ensure_application_settings! - unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true' - settings = retrieve_settings_from_database? - end + return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true' - settings || in_memory_application_settings + cached_application_settings || uncached_application_settings end - def retrieve_settings_from_database? - settings = retrieve_settings_from_database_cache? - return settings if settings.present? - - return fake_application_settings unless connect_to_db? - + def cached_application_settings begin - db_settings = ::ApplicationSetting.current - # In case Redis isn't running or the Redis UNIX socket file is not available + ::ApplicationSetting.cached rescue ::Redis::BaseError, ::Errno::ENOENT - db_settings = ::ApplicationSetting.last + # In case Redis isn't running or the Redis UNIX socket file is not available end - db_settings || ::ApplicationSetting.create_from_defaults end - def retrieve_settings_from_database_cache? + def uncached_application_settings + return fake_application_settings unless connect_to_db? + + # This loads from the database into the cache, so handle Redis errors begin - settings = ApplicationSetting.cached + db_settings = ::ApplicationSetting.current rescue ::Redis::BaseError, ::Errno::ENOENT # In case Redis isn't running or the Redis UNIX socket file is not available - settings = nil end - settings + + # If there are pending migrations, it's possible there are columns that + # need to be added to the application settings. To prevent Rake tasks + # and other callers from failing, use any loaded settings and return + # defaults for missing columns. + if ActiveRecord::Migrator.needs_migration? + defaults = ::ApplicationSetting.defaults + defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present? + return fake_application_settings(defaults) + end + + return db_settings if db_settings.present? + + ::ApplicationSetting.create_from_defaults || in_memory_application_settings end def in_memory_application_settings @@ -62,8 +68,7 @@ module Gitlab active_db_connection = ActiveRecord::Base.connection.active? rescue false active_db_connection && - ActiveRecord::Base.connection.table_exists?('application_settings') && - !ActiveRecord::Migrator.needs_migration? + ActiveRecord::Base.connection.table_exists?('application_settings') rescue ActiveRecord::NoDatabaseError false end diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb index d560dca45c8..58729d3ced8 100644 --- a/lib/gitlab/cycle_analytics/base_query.rb +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -12,17 +12,17 @@ module Gitlab end def stage_query - query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])). - join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). - where(issue_table[:project_id].eq(@project.id)). - where(issue_table[:deleted_at].eq(nil)). - where(issue_table[:created_at].gteq(@options[:from])) + query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])) + .join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) + .where(issue_table[:project_id].eq(@project.id)) + .where(issue_table[:deleted_at].eq(nil)) + .where(issue_table[:created_at].gteq(@options[:from])) # Load merge_requests - query = query.join(mr_table, Arel::Nodes::OuterJoin). - on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])). - join(mr_metrics_table). - on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) + query = query.join(mr_table, Arel::Nodes::OuterJoin) + .on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])) + .join(mr_metrics_table) + .on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) query end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index d0bd1299671..d7dab584a44 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -83,6 +83,22 @@ module Gitlab end end + def self.bulk_insert(table, rows) + return if rows.empty? + + keys = rows.first.keys + columns = keys.map { |key| connection.quote_column_name(key) } + + tuples = rows.map do |row| + row.values_at(*keys).map { |value| connection.quote(value) } + end + + connection.execute <<-EOF + INSERT INTO #{table} (#{columns.join(', ')}) + VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} + EOF + end + # pool_size - The size of the DB pool. # host - An optional host name to use instead of the default one. def self.create_connection_pool(pool_size, host = nil) diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb index 23890e5f493..059054ac9ff 100644 --- a/lib/gitlab/database/median.rb +++ b/lib/gitlab/database/median.rb @@ -29,10 +29,10 @@ module Gitlab end def mysql_median_datetime_sql(arel_table, query_so_far, column_sym) - query = arel_table. - from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)). - project(average([arel_table[column_sym]], 'median')). - where( + query = arel_table + .from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)) + .project(average([arel_table[column_sym]], 'median')) + .where( Arel::Nodes::Between.new( Arel.sql("(select @row_id := @row_id + 1)"), Arel::Nodes::And.new( @@ -67,8 +67,8 @@ module Gitlab cte_table = Arel::Table.new("ordered_records") cte = Arel::Nodes::As.new( cte_table, - arel_table. - project( + arel_table + .project( arel_table[column_sym].as(column_sym.to_s), Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []), Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'), @@ -79,8 +79,8 @@ module Gitlab # From the CTE, select either the middle row or the middle two rows (this is accomplished # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the # selected rows, and this is the median value. - cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")). - where( + cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")) + .where( Arel::Nodes::Between.new( cte_table[:row_id], Arel::Nodes::And.new( @@ -88,9 +88,9 @@ module Gitlab (cte_table[:ct] / Arel.sql('2.0') + 1)] ) ) - ). - with(query_so_far, cte). - to_sql + ) + .with(query_so_far, cte) + .to_sql end private diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index cd85f961242..0643c56db9b 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -222,6 +222,12 @@ module Gitlab # # rubocop: disable Metrics/AbcSize def update_column_in_batches(table, column, value) + if transaction_open? + raise 'update_column_in_batches can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + table = Arel::Table.new(table) count_arel = table.project(Arel.star.count.as('count')) @@ -233,25 +239,31 @@ module Gitlab # Update in batches of 5% until we run out of any rows to update. batch_size = ((total / 100.0) * 5.0).ceil + max_size = 1000 + + # The upper limit is 1000 to ensure we don't lock too many rows. For + # example, for "merge_requests" even 1% of the table is around 35 000 + # rows for GitLab.com. + batch_size = max_size if batch_size > max_size start_arel = table.project(table[:id]).order(table[:id].asc).take(1) start_arel = yield table, start_arel if block_given? start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i loop do - stop_arel = table.project(table[:id]). - where(table[:id].gteq(start_id)). - order(table[:id].asc). - take(1). - skip(batch_size) + stop_arel = table.project(table[:id]) + .where(table[:id].gteq(start_id)) + .order(table[:id].asc) + .take(1) + .skip(batch_size) stop_arel = yield table, stop_arel if block_given? stop_row = exec_query(stop_arel.to_sql).to_hash.first - update_arel = Arel::UpdateManager.new(ActiveRecord::Base). - table(table). - set([[table[column], value]]). - where(table[:id].gteq(start_id)) + update_arel = Arel::UpdateManager.new(ActiveRecord::Base) + .table(table) + .set([[table[column], value]]) + .where(table[:id].gteq(start_id)) if stop_row stop_id = stop_row['id'].to_i @@ -580,15 +592,15 @@ module Gitlab quoted_replacement = Arel::Nodes::Quoted.new(replacement.to_s) if Database.mysql? - locate = Arel::Nodes::NamedFunction. - new('locate', [quoted_pattern, column]) - insert_in_place = Arel::Nodes::NamedFunction. - new('insert', [column, locate, pattern.size, quoted_replacement]) + locate = Arel::Nodes::NamedFunction + .new('locate', [quoted_pattern, column]) + insert_in_place = Arel::Nodes::NamedFunction + .new('insert', [column, locate, pattern.size, quoted_replacement]) Arel::Nodes::SqlLiteral.new(insert_in_place.to_sql) else - replace = Arel::Nodes::NamedFunction. - new("regexp_replace", [column, quoted_pattern, quoted_replacement]) + replace = Arel::Nodes::NamedFunction + .new("regexp_replace", [column, quoted_pattern, quoted_replacement]) Arel::Nodes::SqlLiteral.new(replace.to_sql) end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb index d60fd4bb551..d8163d7da11 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb @@ -27,8 +27,8 @@ module Gitlab new_full_path = join_routable_path(namespace_path, new_path) # skips callbacks & validations - routable.class.where(id: routable). - update_all(path: new_path) + routable.class.where(id: routable) + .update_all(path: new_path) rename_routes(old_full_path, new_full_path) diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb index 2958ad4b8e5..da7e2cb2e85 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb @@ -18,8 +18,8 @@ module Gitlab when :top_level MigrationClasses::Namespace.where(parent_id: nil) end - with_paths = MigrationClasses::Route.arel_table[:path]. - matches_any(path_patterns) + with_paths = MigrationClasses::Route.arel_table[:path] + .matches_any(path_patterns) namespaces.joins(:route).where(with_paths) end @@ -52,15 +52,15 @@ module Gitlab end def repo_paths_for_namespace(namespace) - projects_for_namespace(namespace).distinct.select(:repository_storage). - map(&:repository_storage_path) + projects_for_namespace(namespace).distinct.select(:repository_storage) + .map(&:repository_storage_path) end def projects_for_namespace(namespace) namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id]) - namespace_or_children = MigrationClasses::Project. - arel_table[:namespace_id]. - in(namespace_ids) + namespace_or_children = MigrationClasses::Project + .arel_table[:namespace_id] + .in(namespace_ids) MigrationClasses::Project.where(namespace_or_children) end diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb index 2e197e5cd94..9c9620bc36a 100644 --- a/lib/gitlab/dependency_linker/requirements_txt_linker.rb +++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb @@ -6,7 +6,7 @@ module Gitlab private def link_dependencies - link_regex(/^(?<name>(?![a-z+]+:)[^#.-][^ ><=;\[]+)/) do |name| + link_regex(/^(?<name>(?![a-z+]+:)[^#.-][^ ><=~!;\[]+)/) do |name| "https://pypi.python.org/pypi/#{name}" end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 4212a0dbe2e..d2863a4da71 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -5,7 +5,20 @@ module Gitlab delegate :new_file?, :deleted_file?, :renamed_file?, :old_path, :new_path, :a_mode, :b_mode, :mode_changed?, - :submodule?, :too_large?, :collapsed?, to: :diff, prefix: false + :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, to: :diff, prefix: false + + # Finding a viewer for a diff file happens based only on extension and whether the + # diff file blobs are binary or text, which means 1 diff file should only be matched by 1 viewer, + # and the order of these viewers doesn't really matter. + # + # However, when the diff file blobs are LFS pointers, we cannot know for sure whether the + # file being pointed to is binary or text. In this case, we match only on + # extension, preferring binary viewers over text ones if both exist, since the + # large files referred to in "Large File Storage" are much more likely to be + # binary than text. + RICH_VIEWERS = [ + DiffViewer::Image + ].sort_by { |v| v.binary? ? 0 : 1 }.freeze def initialize(diff, repository:, diff_refs: nil, fallback_diff_refs: nil) @diff = diff @@ -177,6 +190,100 @@ module Gitlab def text? !binary? end + + def external_storage_error? + old_blob&.external_storage_error? || new_blob&.external_storage_error? + end + + def stored_externally? + old_blob&.stored_externally? || new_blob&.stored_externally? + end + + def external_storage + old_blob&.external_storage || new_blob&.external_storage + end + + def content_changed? + old_blob && new_blob && old_blob.id != new_blob.id + end + + def different_type? + old_blob && new_blob && old_blob.binary? != new_blob.binary? + end + + def size + [old_blob&.size, new_blob&.size].compact.sum + end + + def raw_size + [old_blob&.raw_size, new_blob&.raw_size].compact.sum + end + + def raw_binary? + old_blob&.raw_binary? || new_blob&.raw_binary? + end + + def raw_text? + !raw_binary? && !different_type? + end + + def simple_viewer + @simple_viewer ||= simple_viewer_class.new(self) + end + + def rich_viewer + return @rich_viewer if defined?(@rich_viewer) + + @rich_viewer = rich_viewer_class&.new(self) + end + + def rendered_as_text?(ignore_errors: true) + simple_viewer.is_a?(DiffViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?) + end + + private + + def simple_viewer_class + return DiffViewer::NotDiffable unless diffable? + + if content_changed? + if raw_text? + DiffViewer::Text + else + DiffViewer::NoPreview + end + elsif new_file? + if raw_text? + DiffViewer::Text + else + DiffViewer::Added + end + elsif deleted_file? + if raw_text? + DiffViewer::Text + else + DiffViewer::Deleted + end + elsif renamed_file? + DiffViewer::Renamed + elsif mode_changed? + DiffViewer::ModeChanged + end + end + + def rich_viewer_class + viewer_class_from(RICH_VIEWERS) + end + + def viewer_class_from(classes) + return unless diffable? + return if different_type? || external_storage_error? + return unless new_file? || deleted_file? || content_changed? + + verify_binary = !stored_externally? + + classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) } + end end end end diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index bd52ae47e9f..2d89ccfc354 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -42,25 +42,25 @@ module Gitlab end def added? - type == 'new' || type == 'new-nonewline' + %w[new new-nonewline].include?(type) end def removed? - type == 'old' || type == 'old-nonewline' - end - - def rich_text - @parent_file.highlight_lines! if @parent_file && !@rich_text - - @rich_text + %w[old old-nonewline].include?(type) end def meta? - type == 'match' + %w[match new-nonewline old-nonewline].include?(type) end def discussable? - !['match', 'new-nonewline', 'old-nonewline'].include?(type) + !meta? + end + + def rich_text + @parent_file.highlight_lines! if @parent_file && !@rich_text + + @rich_text end def as_json(opts = nil) diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index 481536a380b..0cb26fa45c8 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -14,16 +14,7 @@ module Gitlab lines = [] highlighted_diff_lines = diff_file.highlighted_diff_lines highlighted_diff_lines.each do |line| - if line.meta? || line.unchanged? - # line in the right panel is the same as in the left one - lines << { - left: line, - right: line - } - - free_right_index = nil - i += 1 - elsif line.removed? + if line.removed? lines << { left: line, right: nil @@ -51,6 +42,15 @@ module Gitlab free_right_index = nil i += 1 end + elsif line.meta? || line.unchanged? + # line in the right panel is the same as in the left one + lines << { + left: line, + right: line + } + + free_right_index = nil + i += 1 end end diff --git a/lib/gitlab/downtime_check.rb b/lib/gitlab/downtime_check.rb index ab9537ed7d7..941244694e2 100644 --- a/lib/gitlab/downtime_check.rb +++ b/lib/gitlab/downtime_check.rb @@ -50,8 +50,8 @@ module Gitlab # Returns the class for the given migration file path. def class_for_migration_file(path) - File.basename(path, File.extname(path)).split('_', 2).last.camelize. - constantize + File.basename(path, File.extname(path)).split('_', 2).last.camelize + .constantize end # Returns true if the given migration can be performed without downtime. diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 6d326ee213a..1a5887dab7e 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -76,9 +76,13 @@ module Gitlab step( "Generating the patch against origin/master in #{patch_path}", - %W[git diff --binary origin/master > #{patch_path}] + %w[git diff --binary origin/master...HEAD] ) do |output, status| - throw(:halt_check, :ko) unless status.zero? && File.exist?(patch_path) + throw(:halt_check, :ko) unless status.zero? + + File.write(patch_path, output) + + throw(:halt_check, :ko) unless File.exist?(patch_path) end end @@ -130,7 +134,15 @@ module Gitlab step("Fetching CE/#{ce_branch}", %W[git fetch #{CE_REPO} #{ce_branch}]) step( "Checking if #{patch_path} applies cleanly to EE/master", - %W[git apply --check --3way #{patch_path}] + # Don't use --check here because it can result in a 0-exit status even + # though the patch doesn't apply cleanly, e.g.: + # > git apply --check --3way foo.patch + # error: patch failed: lib/gitlab/ee_compat_check.rb:74 + # Falling back to three-way merge... + # Applied patch to 'lib/gitlab/ee_compat_check.rb' with conflicts. + # > echo $? + # 0 + %W[git apply --3way #{patch_path}] ) do |output, status| puts output unless status.zero? @@ -145,6 +157,7 @@ module Gitlab status = 0 if failed_files.empty? end + command(%w[git reset --hard]) status end end @@ -292,7 +305,7 @@ module Gitlab # In the CE repo $ git fetch origin master - $ git diff --binary origin/master > #{ce_branch}.patch + $ git diff --binary origin/master...HEAD -- > #{ce_branch}.patch # In the EE repo $ git fetch origin master diff --git a/lib/gitlab/email/html_parser.rb b/lib/gitlab/email/html_parser.rb index a4ca62bfc41..50559a48973 100644 --- a/lib/gitlab/email/html_parser.rb +++ b/lib/gitlab/email/html_parser.rb @@ -17,6 +17,13 @@ module Gitlab def filter_replies! document.xpath('//blockquote').each(&:remove) document.xpath('//table').each(&:remove) + + # bogus links with no href are sometimes added by outlook, + # and can result in Html2Text adding extra square brackets + # to the text, so we unwrap them here. + document.xpath('//a[not(@href)]').each do |link| + link.replace(link.children) + end end def filtered_html diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb index 7f884183bb1..1d6f5bb5e1c 100644 --- a/lib/gitlab/etag_caching/middleware.rb +++ b/lib/gitlab/etag_caching/middleware.rb @@ -7,7 +7,7 @@ module Gitlab def call(env) request = Rack::Request.new(env) - route = Gitlab::EtagCaching::Router.match(request) + route = Gitlab::EtagCaching::Router.match(request.path_info) return @app.call(env) unless route track_event(:etag_caching_middleware_used, route) diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index dccc66b3918..75167a6b088 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -53,8 +53,8 @@ module Gitlab ) ].freeze - def self.match(request) - ROUTES.find { |route| route.regexp.match(request.path_info) } + def self.match(path) + ROUTES.find { |route| route.regexp.match(path) } end end end diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb index 0039fc01c8f..072fcfc65e6 100644 --- a/lib/gitlab/etag_caching/store.rb +++ b/lib/gitlab/etag_caching/store.rb @@ -25,6 +25,8 @@ module Gitlab end def redis_key(key) + raise 'Invalid key' if !Rails.env.production? && !Gitlab::EtagCaching::Router.match(key) + "#{REDIS_NAMESPACE}#{key}" end end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 62ddd45785d..a0f46594eb1 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -10,13 +10,21 @@ module Gitlab # ExclusiveLease. # class ExclusiveLease - LUA_CANCEL_SCRIPT = <<-EOS.freeze + LUA_CANCEL_SCRIPT = <<~EOS.freeze local key, uuid = KEYS[1], ARGV[1] if redis.call("get", key) == uuid then redis.call("del", key) end EOS + LUA_RENEW_SCRIPT = <<~EOS.freeze + local key, uuid, ttl = KEYS[1], ARGV[1], ARGV[2] + if redis.call("get", key) == uuid then + redis.call("expire", key, ttl) + return uuid + end + EOS + def self.cancel(key, uuid) Gitlab::Redis.with do |redis| redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_key(key)], argv: [uuid]) @@ -42,6 +50,15 @@ module Gitlab end end + # Try to renew an existing lease. Return lease UUID on success, + # false if the lease is taken by a different UUID or inexistent. + def renew + Gitlab::Redis.with do |redis| + result = redis.eval(LUA_RENEW_SCRIPT, keys: [@redis_key], argv: [@uuid, @timeout]) + result == @uuid + end + end + # Returns true if the key for this lease is set. def exists? Gitlab::Redis.with do |redis| diff --git a/lib/gitlab/fake_application_settings.rb b/lib/gitlab/fake_application_settings.rb new file mode 100644 index 00000000000..bb14a8cd9e7 --- /dev/null +++ b/lib/gitlab/fake_application_settings.rb @@ -0,0 +1,27 @@ +# This class extends an OpenStruct object by adding predicate methods to mimic +# ActiveRecord access. We rely on the initial values being true or false to +# determine whether to define a predicate method because for a newly-added +# column that has not been migrated yet, there is no way to determine the +# column type without parsing db/schema.rb. +module Gitlab + class FakeApplicationSettings < OpenStruct + def initialize(options = {}) + super + + FakeApplicationSettings.define_predicate_methods(options) + end + + # Mimic ActiveRecord predicate methods for boolean values + def self.define_predicate_methods(options) + options.each do |key, value| + next if key.to_s.end_with?('?') + next unless [true, false].include?(value) + + define_method "#{key}?" do + actual_key = key.to_s.chomp('?') + self[actual_key] + end + end + end + end +end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 33a7624e303..a7aceab4c14 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -14,6 +14,51 @@ module Gitlab class << self def find(repository, sha, path) + Gitlab::GitalyClient.migrate(:project_raw_show) do |is_enabled| + if is_enabled + find_by_gitaly(repository, sha, path) + else + find_by_rugged(repository, sha, path) + end + end + end + + def find_by_gitaly(repository, sha, path) + path = path.sub(/\A\/*/, '') + path = '/' if path.empty? + name = File.basename(path) + entry = Gitlab::GitalyClient::Commit.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE) + return unless entry + + case entry.type + when :COMMIT + new( + id: entry.oid, + name: name, + size: 0, + data: '', + path: path, + commit_id: sha + ) + when :BLOB + # EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks + # only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15), + # which is what we use below to keep a consistent behavior. + detect = CharlockHolmes::EncodingDetector.new(8000).detect(entry.data) + new( + id: entry.oid, + name: name, + size: entry.size, + data: entry.data.dup, + mode: entry.mode.to_s(8), + path: path, + commit_id: sha, + binary: detect && detect[:type] == :binary + ) + end + end + + def find_by_rugged(repository, sha, path) commit = repository.lookup(sha) root_tree = commit.tree diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index bb04731f08c..9c0606d780a 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -4,7 +4,7 @@ module Gitlab class Commit include Gitlab::EncodingHelper - attr_accessor :raw_commit, :head, :refs + attr_accessor :raw_commit, :head SERIALIZE_KEYS = [ :id, :message, :parent_ids, @@ -104,9 +104,63 @@ module Gitlab [] end - # Delegate Repository#find_commits + # Returns commits collection + # + # Ex. + # Commit.find_all( + # repo, + # ref: 'master', + # max_count: 10, + # skip: 5, + # order: :date + # ) + # + # +options+ is a Hash of optional arguments to git + # :ref is the ref from which to begin (SHA1 or name) + # :max_count is the maximum number of commits to fetch + # :skip is the number of commits to skip + # :order is the commits order and allowed value is :none (default), :date, + # :topo, or any combination of them (in an array). Commit ordering types + # are documented here: + # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant) + # def find_all(repo, options = {}) - repo.find_commits(options) + actual_options = options.dup + + allowed_options = [:ref, :max_count, :skip, :order] + + actual_options.keep_if do |key| + allowed_options.include?(key) + end + + default_options = { skip: 0 } + actual_options = default_options.merge(actual_options) + + rugged = repo.rugged + walker = Rugged::Walker.new(rugged) + + if actual_options[:ref] + walker.push(rugged.rev_parse_oid(actual_options[:ref])) + else + rugged.references.each("refs/heads/*") do |ref| + walker.push(ref.target_id) + end + end + + walker.sorting(rugged_sort_type(actual_options[:order])) + + commits = [] + offset = actual_options[:skip] + limit = actual_options[:max_count] + walker.each(offset: offset, limit: limit) do |commit| + commits.push(decorate(commit)) + end + + walker.reset + + commits + rescue Rugged::OdbError + [] end def decorate(commit, ref = nil) @@ -131,6 +185,20 @@ module Gitlab diff.find_similar!(break_rewrites: break_rewrites) diff end + + # Returns the `Rugged` sorting type constant for one or more given + # sort types. Valid keys are `:none`, `:topo`, and `:date`, or an array + # containing more than one of them. `:date` uses a combination of date and + # topological sorting to closer mimic git's native ordering. + def rugged_sort_type(sort_type) + @rugged_sort_types ||= { + none: Rugged::SORT_NONE, + topo: Rugged::SORT_TOPO, + date: Rugged::SORT_DATE | Rugged::SORT_TOPO + } + + @rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE) + end end def initialize(raw_commit, head = nil) @@ -175,8 +243,8 @@ module Gitlab # Shows the diff between the commit's parent and the commit. # # Cuts out the header and stats from #to_patch and returns only the diff. - def to_diff(options = {}) - diff_from_parent(options).patch + def to_diff + diff_from_parent.patch end # Returns a diff object for the changes from this commit's first parent. diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 88ad760bea3..cf7829a583b 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -16,9 +16,11 @@ module Gitlab alias_method :renamed_file?, :renamed_file attr_accessor :expanded + attr_writer :too_large - # We need this accessor because of `to_hash` and `init_from_hash` - attr_accessor :too_large + alias_method :expanded?, :expanded + + SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze class << self # The maximum size of a diff to display. @@ -229,16 +231,10 @@ module Gitlab end end - def serialize_keys - @serialize_keys ||= %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large) - end - def to_hash hash = {} - keys = serialize_keys - - keys.each do |key| + SERIALIZE_KEYS.each do |key| hash[key] = send(key) end @@ -265,6 +261,9 @@ module Gitlab end end + # This is used by `to_hash` and `init_from_hash`. + alias_method :too_large, :too_large? + def too_large! @diff = '' @line_count = 0 @@ -313,13 +312,13 @@ module Gitlab def init_from_hash(hash) raw_diff = hash.symbolize_keys - serialize_keys.each do |key| + SERIALIZE_KEYS.each do |key| send(:"#{key}=", raw_diff[key.to_sym]) end end def init_from_gitaly(diff) - @diff = diff.patch if diff.respond_to?(:patch) + @diff = encode!(diff.patch) if diff.respond_to?(:patch) @new_path = encode!(diff.to_path.dup) @old_path = encode!(diff.from_path.dup) @a_mode = diff.old_mode.to_s(8) diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 334e06a6eca..555894907cc 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -97,7 +97,7 @@ module Gitlab diff = Gitlab::Git::Diff.new(raw, expanded: expanded) - if !expanded && over_safe_limits?(i) + if !expanded && over_safe_limits?(i) && diff.line_count > 0 diff.collapse! end diff --git a/lib/gitlab/git/gitmodules_parser.rb b/lib/gitlab/git/gitmodules_parser.rb new file mode 100644 index 00000000000..f4e3b5e5129 --- /dev/null +++ b/lib/gitlab/git/gitmodules_parser.rb @@ -0,0 +1,77 @@ +module Gitlab + module Git + class GitmodulesParser + def initialize(content) + @content = content + end + + # Parses the contents of a .gitmodules file and returns a hash of + # submodule information, indexed by path. + def parse + reindex_by_path(get_submodules_by_name) + end + + private + + class State + def initialize + @result = {} + @current_submodule = nil + end + + def start_section(section) + # In some .gitmodules files (e.g. nodegit's), a header + # with the same name appears multiple times; we want to + # accumulate the configs across these + @current_submodule = @result[section] || { 'name' => section } + @result[section] = @current_submodule + end + + def set_attribute(attr, value) + @current_submodule[attr] = value + end + + def section_started? + !@current_submodule.nil? + end + + def submodules_by_name + @result + end + end + + def get_submodules_by_name + iterator = State.new + + @content.split("\n").each_with_object(iterator) do |text, iterator| + next if text =~ /^\s*#/ + + if text =~ /\A\[submodule "(?<name>[^"]+)"\]\z/ + iterator.start_section($~[:name]) + else + next unless iterator.section_started? + + next unless text =~ /\A\s*(?<key>\w+)\s*=\s*(?<value>.*)\z/ + + value = $~[:value].chomp + iterator.set_attribute($~[:key], value) + end + end + + iterator.submodules_by_name + end + + def reindex_by_path(submodules_by_name) + # Convert from an indexed by name to an array indexed by path + # If a submodule doesn't have a path, it is considered bogus + # and is ignored + submodules_by_name.each_with_object({}) do |(name, data), results| + path = data.delete 'path' + next unless path + + results[path] = data + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 85695d0a4df..dd296983491 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -494,70 +494,6 @@ module Gitlab end end - # Returns commits collection - # - # Ex. - # repo.find_commits( - # ref: 'master', - # max_count: 10, - # skip: 5, - # order: :date - # ) - # - # +options+ is a Hash of optional arguments to git - # :ref is the ref from which to begin (SHA1 or name) - # :contains is the commit contained by the refs from which to begin (SHA1 or name) - # :max_count is the maximum number of commits to fetch - # :skip is the number of commits to skip - # :order is the commits order and allowed value is :none (default), :date, - # :topo, or any combination of them (in an array). Commit ordering types - # are documented here: - # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant) - # - def find_commits(options = {}) - actual_options = options.dup - - allowed_options = [:ref, :max_count, :skip, :contains, :order] - - actual_options.keep_if do |key| - allowed_options.include?(key) - end - - default_options = { skip: 0 } - actual_options = default_options.merge(actual_options) - - walker = Rugged::Walker.new(rugged) - - if actual_options[:ref] - walker.push(rugged.rev_parse_oid(actual_options[:ref])) - elsif actual_options[:contains] - branches_contains(actual_options[:contains]).each do |branch| - walker.push(branch.target_id) - end - else - rugged.references.each("refs/heads/*") do |ref| - walker.push(ref.target_id) - end - end - - sort_type = rugged_sort_type(actual_options[:order]) - walker.sorting(sort_type) - - commits = [] - offset = actual_options[:skip] - limit = actual_options[:max_count] - walker.each(offset: offset, limit: limit) do |commit| - gitlab_commit = Gitlab::Git::Commit.decorate(commit) - commits.push(gitlab_commit) - end - - walker.reset - - commits - rescue Rugged::OdbError - [] - end - # Returns branch names collection that contains the special commit(SHA1 # or name) # @@ -613,31 +549,20 @@ module Gitlab rugged.rev_parse(oid_or_ref_name) end - # Return hash with submodules info for this repository + # Returns url for submodule # # Ex. - # { - # "rack" => { - # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320", - # "path" => "rack", - # "url" => "git://github.com/chneukirchen/rack.git" - # }, - # "encoding" => { - # "id" => .... - # } - # } + # @repository.submodule_url_for('master', 'rack') + # # => git@localhost:rack.git # - def submodules(ref) - commit = rev_parse_target(ref) - return {} unless commit + def submodule_url_for(ref, path) + if submodules(ref).any? + submodule = submodules(ref)[path] - begin - content = blob_content(commit, ".gitmodules") - rescue InvalidBlobName - return {} + if submodule + submodule['url'] + end end - - parse_gitmodules(commit, content) end # Return total commits count accessible from passed ref @@ -975,6 +900,23 @@ module Gitlab private + # We are trying to deprecate this method because it does a lot of work + # but it seems to be used only to look up submodule URL's. + # https://gitlab.com/gitlab-org/gitaly/issues/329 + def submodules(ref) + commit = rev_parse_target(ref) + return {} unless commit + + begin + content = blob_content(commit, ".gitmodules") + rescue InvalidBlobName + return {} + end + + parser = GitmodulesParser.new(content) + fill_submodule_ids(commit, parser.parse) + end + def alternate_object_directories Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact end @@ -998,42 +940,19 @@ module Gitlab end end - # Parses the contents of a .gitmodules file and returns a hash of - # submodule information. - def parse_gitmodules(commit, content) - modules = {} - - name = nil - content.each_line do |line| - case line.strip - when /\A\[submodule "(?<name>[^"]+)"\]\z/ # Submodule header - name = $~[:name] - modules[name] = {} - when /\A(?<key>\w+)\s*=\s*(?<value>.*)\z/ # Key/value pair - key = $~[:key] - value = $~[:value].chomp - - next unless name && modules[name] - - modules[name][key] = value - - if key == 'path' - begin - modules[name]['id'] = blob_content(commit, value) - rescue InvalidBlobName - # The current entry is invalid - modules.delete(name) - name = nil - end - end - when /\A#/ # Comment - next - else # Invalid line - name = nil + # Fill in the 'id' field of a submodule hash from its values + # as-of +commit+. Return a Hash consisting only of entries + # from the submodule hash for which the 'id' field is filled. + def fill_submodule_ids(commit, submodule_data) + submodule_data.each do |path, data| + id = begin + blob_content(commit, path) + rescue InvalidBlobName + nil end + data['id'] = id end - - modules + submodule_data.select { |path, data| data['id'] } end # Returns true if +commit+ introduced changes to +path+, using commit @@ -1250,20 +1169,6 @@ module Gitlab rescue GRPC::BadStatus => e raise CommandError.new(e) end - - # Returns the `Rugged` sorting type constant for one or more given - # sort types. Valid keys are `:none`, `:topo`, and `:date`, or an array - # containing more than one of them. `:date` uses a combination of date and - # topological sorting to closer mimic git's native ordering. - def rugged_sort_type(sort_type) - @rugged_sort_types ||= { - none: Rugged::SORT_NONE, - topo: Rugged::SORT_TOPO, - date: Rugged::SORT_DATE | Rugged::SORT_TOPO - } - - @rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE) - end end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 0a19d24eb20..0b62911958d 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -22,12 +22,13 @@ module Gitlab PUSH_COMMANDS = %w{ git-receive-pack }.freeze ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS - attr_reader :actor, :project, :protocol, :authentication_abilities + attr_reader :actor, :project, :protocol, :authentication_abilities, :redirected_path - def initialize(actor, project, protocol, authentication_abilities:) + def initialize(actor, project, protocol, authentication_abilities:, redirected_path: nil) @actor = actor @project = project @protocol = protocol + @redirected_path = redirected_path @authentication_abilities = authentication_abilities end @@ -35,6 +36,7 @@ module Gitlab check_protocol! check_active_user! check_project_accessibility! + check_project_moved! check_command_disabled!(cmd) check_command_existence!(cmd) check_repository_existence! @@ -87,6 +89,21 @@ module Gitlab end end + def check_project_moved! + if redirected_path + url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo + message = <<-MESSAGE.strip_heredoc + Project '#{redirected_path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{url} + MESSAGE + + raise NotFoundError, message + end + end + def check_command_disabled!(cmd) if upload_pack?(cmd) check_upload_pack_disabled! diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 2343446bf22..f605c06dfc3 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -1,3 +1,5 @@ +require 'base64' + require 'gitaly' module Gitlab @@ -48,6 +50,26 @@ module Gitlab address end + # All Gitaly RPC call sites should use GitalyClient.call. This method + # makes sure that per-request authentication headers are set. + def self.call(storage, service, rpc, request) + metadata = request_metadata(storage) + metadata = yield(metadata) if block_given? + stub(service, storage).send(rpc, request, metadata) + end + + def self.request_metadata(storage) + encoded_token = Base64.strict_encode64(token(storage).to_s) + { metadata: { 'authorization' => "Bearer #{encoded_token}" } } + end + + def self.token(storage) + params = Gitlab.config.repositories.storages[storage] + raise "storage not found: #{storage.inspect}" if params.nil? + + params['gitaly_token'].presence || Gitlab.config.gitaly['token'] + end + def self.enabled? Gitlab.config.gitaly.enabled end diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb index ba3da781dad..b8877619797 100644 --- a/lib/gitlab/gitaly_client/commit.rb +++ b/lib/gitlab/gitaly_client/commit.rb @@ -11,33 +11,51 @@ module Gitlab end def is_ancestor(ancestor_id, child_id) - stub = GitalyClient.stub(:commit, @repository.storage) request = Gitaly::CommitIsAncestorRequest.new( repository: @gitaly_repo, ancestor_id: ancestor_id, child_id: child_id ) - stub.commit_is_ancestor(request).value + GitalyClient.call(@repository.storage, :commit, :commit_is_ancestor, request).value end def diff_from_parent(commit, options = {}) request_params = commit_diff_request_params(commit, options) request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false) - - response = diff_service_stub.commit_diff(Gitaly::CommitDiffRequest.new(request_params)) + request = Gitaly::CommitDiffRequest.new(request_params) + response = GitalyClient.call(@repository.storage, :diff, :commit_diff, request) Gitlab::Git::DiffCollection.new(GitalyClient::DiffStitcher.new(response), options) end def commit_deltas(commit) - request_params = commit_diff_request_params(commit) - - response = diff_service_stub.commit_delta(Gitaly::CommitDeltaRequest.new(request_params)) + request = Gitaly::CommitDeltaRequest.new(commit_diff_request_params(commit)) + response = GitalyClient.call(@repository.storage, :diff, :commit_delta, request) response.flat_map do |msg| msg.deltas.map { |d| Gitlab::Git::Diff.new(d) } end end + def tree_entry(ref, path, limit = nil) + request = Gitaly::TreeEntryRequest.new( + repository: @gitaly_repo, + revision: ref, + path: path.dup.force_encoding(Encoding::ASCII_8BIT), + limit: limit.to_i + ) + + response = GitalyClient.call(@repository.storage, :commit, :tree_entry, request) + entry = response.first + return unless entry.oid.present? + + if entry.type == :BLOB + rest_of_data = response.reduce("") { |memo, msg| memo << msg.data } + entry.data += rest_of_data + end + + entry + end + private def commit_diff_request_params(commit, options = {}) @@ -50,10 +68,6 @@ module Gitlab paths: options.fetch(:paths, []) } end - - def diff_service_stub - GitalyClient.stub(:diff, @repository.storage) - end end end end diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb index d84e8d752dc..65d81dc5d46 100644 --- a/lib/gitlab/gitaly_client/diff_stitcher.rb +++ b/lib/gitlab/gitaly_client/diff_stitcher.rb @@ -13,7 +13,10 @@ module Gitlab @rpc_response.each do |diff_msg| if current_diff.nil? diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::FIELDS) - diff_params[:patch] = diff_msg.raw_patch_data + # gRPC uses frozen strings by default, and we need to have an unfrozen string as it + # gets processed further down the line. So we unfreeze the first chunk of the patch + # in case it's the only chunk we receive for this diff. + diff_params[:patch] = diff_msg.raw_patch_data.dup current_diff = GitalyClient::Diff.new(diff_params) else diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb index 719554eac52..78ed433e6b8 100644 --- a/lib/gitlab/gitaly_client/notifications.rb +++ b/lib/gitlab/gitaly_client/notifications.rb @@ -1,17 +1,19 @@ module Gitlab module GitalyClient class Notifications - attr_accessor :stub - # 'repository' is a Gitlab::Git::Repository def initialize(repository) @gitaly_repo = repository.gitaly_repository - @stub = GitalyClient.stub(:notifications, repository.storage) + @storage = repository.storage end def post_receive - request = Gitaly::PostReceiveRequest.new(repository: @gitaly_repo) - @stub.post_receive(request) + GitalyClient.call( + @storage, + :notifications, + :post_receive, + Gitaly::PostReceiveRequest.new(repository: @gitaly_repo) + ) end end end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index 227fe45642e..6d5f54dd959 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -1,29 +1,28 @@ module Gitlab module GitalyClient class Ref - attr_accessor :stub - # 'repository' is a Gitlab::Git::Repository def initialize(repository) @gitaly_repo = repository.gitaly_repository - @stub = GitalyClient.stub(:ref, repository.storage) + @storage = repository.storage end def default_branch_name request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo) - branch_name = stub.find_default_branch_name(request).name - - Gitlab::Git.branch_name(branch_name) + response = GitalyClient.call(@storage, :ref, :find_default_branch_name, request) + Gitlab::Git.branch_name(response.name) end def branch_names request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo) - consume_refs_response(stub.find_all_branch_names(request), prefix: 'refs/heads/') + response = GitalyClient.call(@storage, :ref, :find_all_branch_names, request) + consume_refs_response(response, prefix: 'refs/heads/') end def tag_names request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo) - consume_refs_response(stub.find_all_tag_names(request), prefix: 'refs/tags/') + response = GitalyClient.call(@storage, :ref, :find_all_tag_names, request) + consume_refs_response(response, prefix: 'refs/tags/') end def find_ref_name(commit_id, ref_prefix) @@ -32,8 +31,7 @@ module Gitlab commit_id: commit_id, prefix: ref_prefix ) - - stub.find_ref_name(request).name + GitalyClient.call(@storage, :ref, :find_ref_name, request).name end def count_tag_names @@ -47,7 +45,8 @@ module Gitlab def local_branches(sort_by: nil) request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo) request.sort_by = sort_by_param(sort_by) if sort_by - consume_branches_response(stub.find_local_branches(request)) + response = GitalyClient.call(@storage, :ref, :find_local_branches, request) + consume_branches_response(response) end private diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index 86d055d3533..f5a4c5493ef 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -4,7 +4,6 @@ module Gitlab class << self def repository(repository_storage, relative_path) Gitaly::Repository.new( - path: File.join(Gitlab.config.repositories.storages[repository_storage]['path'], relative_path), storage_name: repository_storage, relative_path: relative_path ) diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb index e9d5d52cabb..5a31e56cb30 100644 --- a/lib/gitlab/group_hierarchy.rb +++ b/lib/gitlab/group_hierarchy.rb @@ -3,33 +3,38 @@ module Gitlab # # This class uses recursive CTEs and as a result will only work on PostgreSQL. class GroupHierarchy - attr_reader :base, :model - - # base - An instance of ActiveRecord::Relation for which to get parent or - # child groups. - def initialize(base) - @base = base - @model = base.model + attr_reader :ancestors_base, :descendants_base, :model + + # ancestors_base - An instance of ActiveRecord::Relation for which to + # get parent groups. + # descendants_base - An instance of ActiveRecord::Relation for which to + # get child groups. If omitted, ancestors_base is used. + def initialize(ancestors_base, descendants_base = ancestors_base) + raise ArgumentError.new("Model of ancestors_base does not match model of descendants_base") if ancestors_base.model != descendants_base.model + + @ancestors_base = ancestors_base + @descendants_base = descendants_base + @model = ancestors_base.model end - # Returns a relation that includes the base set of groups and all their - # ancestors (recursively). + # Returns a relation that includes the ancestors_base set of groups + # and all their ancestors (recursively). def base_and_ancestors - return model.none unless Group.supports_nested_groups? + return ancestors_base unless Group.supports_nested_groups? base_and_ancestors_cte.apply_to(model.all) end - # Returns a relation that includes the base set of groups and all their - # descendants (recursively). + # Returns a relation that includes the descendants_base set of groups + # and all their descendants (recursively). def base_and_descendants - return model.none unless Group.supports_nested_groups? + return descendants_base unless Group.supports_nested_groups? base_and_descendants_cte.apply_to(model.all) end - # Returns a relation that includes the base groups, their ancestors, and the - # descendants of the base groups. + # Returns a relation that includes the base groups, their ancestors, + # and the descendants of the base groups. # # The resulting query will roughly look like the following: # @@ -48,8 +53,10 @@ module Gitlab # # Using this approach allows us to further add criteria to the relation with # Rails thinking it's selecting data the usual way. + # + # If nested groups are not supported, ancestors_base is returned. def all_groups - return base unless Group.supports_nested_groups? + return ancestors_base unless Group.supports_nested_groups? ancestors = base_and_ancestors_cte descendants = base_and_descendants_cte @@ -60,11 +67,11 @@ module Gitlab union = SQL::Union.new([model.unscoped.from(ancestors_table), model.unscoped.from(descendants_table)]) - model. - unscoped. - with. - recursive(ancestors.to_arel, descendants.to_arel). - from("(#{union.to_sql}) #{model.table_name}") + model + .unscoped + .with + .recursive(ancestors.to_arel, descendants.to_arel) + .from("(#{union.to_sql}) #{model.table_name}") end private @@ -72,13 +79,13 @@ module Gitlab def base_and_ancestors_cte cte = SQL::RecursiveCTE.new(:base_and_ancestors) - cte << base.except(:order) + cte << ancestors_base.except(:order) # Recursively get all the ancestors of the base set. - cte << model. - from([groups_table, cte.table]). - where(groups_table[:id].eq(cte.table[:parent_id])). - except(:order) + cte << model + .from([groups_table, cte.table]) + .where(groups_table[:id].eq(cte.table[:parent_id])) + .except(:order) cte end @@ -86,13 +93,13 @@ module Gitlab def base_and_descendants_cte cte = SQL::RecursiveCTE.new(:base_and_descendants) - cte << base.except(:order) + cte << descendants_base.except(:order) # Recursively get all the descendants of the base set. - cte << model. - from([groups_table, cte.table]). - where(groups_table[:parent_id].eq(cte.table[:id])). - except(:order) + cte << model + .from([groups_table, cte.table]) + .where(groups_table[:parent_id].eq(cte.table[:id])) + .except(:order) cte end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 6b24da030df..5408a1a6838 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -1,8 +1,8 @@ module Gitlab class Highlight def self.highlight(blob_name, blob_content, repository: nil, plain: false) - new(blob_name, blob_content, repository: repository). - highlight(blob_content, continue: false, plain: plain) + new(blob_name, blob_content, repository: repository) + .highlight(blob_content, continue: false, plain: plain) end attr_reader :blob_name diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 328dd17e452..db7cdf4b5c7 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -6,11 +6,13 @@ module Gitlab 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch', + 'fr' => 'Français', 'pt_BR' => 'Português(Brasil)', 'zh_CN' => '简体中文', 'zh_HK' => '繁體中文(香港)', 'zh_TW' => '繁體中文(臺灣)', - 'bg' => 'български' + 'bg' => 'български', + 'eo' => 'Esperanto' }.freeze def available_locales diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 27d5a9198b6..3470a09eaf0 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -3,7 +3,7 @@ module Gitlab extend self # For every version update, the version history in import_export.md has to be kept up to date. - VERSION = '0.1.7'.freeze + VERSION = '0.1.8'.freeze FILENAME_LIMIT = 50 def export_path(relative_path:) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ff2b1d08c3c..1860352c96d 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -26,7 +26,8 @@ project_tree: - notes: - :author - :events - - :merge_request_diff + - merge_request_diff: + - :merge_request_diff_files - :events - :timelogs - label_links: @@ -92,10 +93,13 @@ excluded_attributes: - :expired_at merge_request_diff: - :st_diffs + merge_request_diff_files: + - :diff issues: - :milestone_id merge_requests: - :milestone_id + - :ref_fetched award_emoji: - :awardable_id statuses: @@ -113,6 +117,8 @@ methods: - :type merge_request_diff: - :utf8_st_diffs + merge_request_diff_files: + - :utf8_diff merge_requests: - :diff_head_sha project: diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb index 48c09dafcb6..b48f63bcd7e 100644 --- a/lib/gitlab/import_export/json_hash_builder.rb +++ b/lib/gitlab/import_export/json_hash_builder.rb @@ -83,7 +83,9 @@ module Gitlab # +value+ existing model to be included in the hash # +json_config_hash+ the original hash containing the root model def add_model_value(current_key, value, json_config_hash) - @attributes_finder.parse(value) { |hash| value = { value => hash } } + @attributes_finder.parse(value) do |hash| + value = { value => hash } unless value.is_a?(Hash) + end add_to_array(current_key, json_config_hash, value) end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 695852526cb..20580459046 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -71,6 +71,7 @@ module Gitlab @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] set_st_diff_commits if @relation_name == :merge_request_diff + set_diff if @relation_name == :merge_request_diff_files end def update_user_references @@ -202,6 +203,10 @@ module Gitlab HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits']) end + def set_diff + @relation_hash['diff'] = @relation_hash.delete('utf8_diff') + end + def existing_or_new_object # Only find existing records to avoid mapping tables such as milestones # Otherwise always create the record, skipping the extra SELECT clause. diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb index 8db91d25a4b..208f0e1bbea 100644 --- a/lib/gitlab/job_waiter.rb +++ b/lib/gitlab/job_waiter.rb @@ -14,7 +14,7 @@ module Gitlab # timeout - The maximum amount of seconds to block the caller for. This # ensures we don't indefinitely block a caller in case a job takes # long to process, or is never processed. - def wait(timeout = 60) + def wait(timeout = 10) start = Time.current while (Time.current - start) <= timeout diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index 4a6091488c8..c56c1a4322f 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -8,13 +8,13 @@ module Gitlab ) # Filters an array of pods (as returned by the kubernetes API) by their labels - def filter_pods(pods, labels = {}) - pods.select do |pod| - metadata = pod.fetch("metadata", {}) - pod_labels = metadata.fetch("labels", nil) - next unless pod_labels + def filter_by_label(items, labels = {}) + items.select do |item| + metadata = item.fetch("metadata", {}) + item_labels = metadata.fetch("labels", nil) + next unless item_labels - labels.all? { |k, v| pod_labels[k.to_s] == v } + labels.all? { |k, v| item_labels[k.to_s] == v } end end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 54a5b1d31cd..8779577258b 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -16,8 +16,8 @@ module Gitlab def self.allowed?(user) self.open(user) do |access| if access.allowed? - user.last_credential_check_at = Time.now - user.save + Users::UpdateService.new(user, last_credential_check_a: Time.now).execute + true else false diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 5e299e26c54..39180dc17d9 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -10,9 +10,9 @@ module Gitlab class << self def find_by_uid_and_provider(uid, provider) # LDAP distinguished name is case-insensitive - identity = ::Identity. - where(provider: provider). - iwhere(extern_uid: uid).last + identity = ::Identity + .where(provider: provider) + .iwhere(extern_uid: uid).last identity && identity.user end end diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb index 3a39791edbf..d7c56463aac 100644 --- a/lib/gitlab/metrics/influx_db.rb +++ b/lib/gitlab/metrics/influx_db.rb @@ -157,8 +157,8 @@ module Gitlab host = settings[:host] port = settings[:port] - InfluxDB::Client. - new(udp: { host: host, port: port }) + InfluxDB::Client + .new(udp: { host: host, port: port }) end end end diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index 60686509332..9d314a56e58 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -5,8 +5,16 @@ module Gitlab module Prometheus include Gitlab::CurrentSettings + def metrics_folder_present? + ENV.has_key?('prometheus_multiproc_dir') && + ::Dir.exist?(ENV['prometheus_multiproc_dir']) && + ::File.writable?(ENV['prometheus_multiproc_dir']) + end + def prometheus_metrics_enabled? - @prometheus_metrics_enabled ||= current_application_settings[:prometheus_metrics_enabled] || false + return @prometheus_metrics_enabled if defined?(@prometheus_metrics_enabled) + + @prometheus_metrics_enabled = prometheus_metrics_enabled_unmemoized end def registry @@ -36,6 +44,12 @@ module Gitlab NullMetric.new end end + + private + + def prometheus_metrics_enabled_unmemoized + metrics_folder_present? && current_application_settings[:prometheus_metrics_enabled] || false + end end end end diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb index 3aaebb3e9c3..aba3e0df382 100644 --- a/lib/gitlab/metrics/system.rb +++ b/lib/gitlab/metrics/system.rb @@ -34,13 +34,13 @@ module Gitlab # THREAD_CPUTIME is not supported on OS X if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID) def self.cpu_time - Process. - clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond) + Process + .clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond) end else def self.cpu_time - Process. - clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond) + Process + .clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond) end end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 7307f8c2c87..b3f453e506d 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -32,7 +32,7 @@ module Gitlab block_after_save = needs_blocking? - gl_user.save! + Users::UpdateService.new(gl_user).execute! gl_user.block if block_after_save diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb index 31a24460f0f..fc3f21233dd 100644 --- a/lib/gitlab/other_markup.rb +++ b/lib/gitlab/other_markup.rb @@ -6,8 +6,8 @@ module Gitlab # input - the source text in a markup format # def self.render(file_name, input, context) - html = GitHub::Markup.render(file_name, input). - force_encoding(input.encoding) + html = GitHub::Markup.render(file_name, input) + .force_encoding(input.encoding) context[:pipeline] = :markup html = Banzai.render(html, context) diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb index 7ab80f5ee0f..574ae8731a5 100644 --- a/lib/gitlab/performance_bar/peek_query_tracker.rb +++ b/lib/gitlab/performance_bar/peek_query_tracker.rb @@ -3,8 +3,8 @@ module Gitlab module PerformanceBar module PeekQueryTracker def sorted_queries - PEEK_DB_CLIENT.query_details. - sort { |a, b| b[:duration] <=> a[:duration] } + PEEK_DB_CLIENT.query_details + .sort { |a, b| b[:duration] <=> a[:duration] } end def results diff --git a/lib/gitlab/project_authorizations/with_nested_groups.rb b/lib/gitlab/project_authorizations/with_nested_groups.rb index bb0df1e3dad..15b8beacf60 100644 --- a/lib/gitlab/project_authorizations/with_nested_groups.rb +++ b/lib/gitlab/project_authorizations/with_nested_groups.rb @@ -28,34 +28,34 @@ module Gitlab # Projects that belong directly to any of the groups the user has # access to. - Namespace. - unscoped. - select([alias_as_column(projects[:id], 'project_id'), - cte_alias[:access_level]]). - from(cte_alias). - joins(:projects), + Namespace + .unscoped + .select([alias_as_column(projects[:id], 'project_id'), + cte_alias[:access_level]]) + .from(cte_alias) + .joins(:projects), # Projects shared with any of the namespaces the user has access to. - Namespace. - unscoped. - select([links[:project_id], - least(cte_alias[:access_level], - links[:group_access], - 'access_level')]). - from(cte_alias). - joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id'). - joins('INNER JOIN projects ON projects.id = project_group_links.project_id'). - joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id'). - where('p_ns.share_with_group_lock IS FALSE') + Namespace + .unscoped + .select([links[:project_id], + least(cte_alias[:access_level], + links[:group_access], + 'access_level')]) + .from(cte_alias) + .joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id') + .joins('INNER JOIN projects ON projects.id = project_group_links.project_id') + .joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id') + .where('p_ns.share_with_group_lock IS FALSE') ] union = Gitlab::SQL::Union.new(relations) - ProjectAuthorization. - unscoped. - with. - recursive(cte.to_arel). - select_from_union(union) + ProjectAuthorization + .unscoped + .with + .recursive(cte.to_arel) + .select_from_union(union) end private @@ -68,17 +68,17 @@ module Gitlab namespaces = Namespace.arel_table # Namespaces the user is a member of. - cte << user.groups. - select([namespaces[:id], members[:access_level]]). - except(:order) + cte << user.groups + .select([namespaces[:id], members[:access_level]]) + .except(:order) # Sub groups of any groups the user is a member of. cte << Group.select([namespaces[:id], greatest(members[:access_level], - cte.table[:access_level], 'access_level')]). - joins(join_cte(cte)). - joins(join_members). - except(:order) + cte.table[:access_level], 'access_level')]) + .joins(join_cte(cte)) + .joins(join_members) + .except(:order) cte end @@ -88,11 +88,11 @@ module Gitlab members = Member.arel_table namespaces = Namespace.arel_table - cond = members[:source_id]. - eq(namespaces[:id]). - and(members[:source_type].eq('Namespace')). - and(members[:requested_at].eq(nil)). - and(members[:user_id].eq(user.id)) + cond = members[:source_id] + .eq(namespaces[:id]) + .and(members[:source_type].eq('Namespace')) + .and(members[:requested_at].eq(nil)) + .and(members[:user_id].eq(user.id)) Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond)) end diff --git a/lib/gitlab/project_authorizations/without_nested_groups.rb b/lib/gitlab/project_authorizations/without_nested_groups.rb index 627e8c5fba2..ad87540e6c2 100644 --- a/lib/gitlab/project_authorizations/without_nested_groups.rb +++ b/lib/gitlab/project_authorizations/without_nested_groups.rb @@ -26,9 +26,9 @@ module Gitlab union = Gitlab::SQL::Union.new(relations) - ProjectAuthorization. - unscoped. - select_from_union(union) + ProjectAuthorization + .unscoped + .select_from_union(union) end end end diff --git a/lib/gitlab/prometheus/additional_metrics_parser.rb b/lib/gitlab/prometheus/additional_metrics_parser.rb new file mode 100644 index 00000000000..cb95daf2260 --- /dev/null +++ b/lib/gitlab/prometheus/additional_metrics_parser.rb @@ -0,0 +1,34 @@ +module Gitlab + module Prometheus + module AdditionalMetricsParser + extend self + + def load_groups_from_yaml + additional_metrics_raw.map(&method(:group_from_entry)) + end + + private + + def validate!(obj) + raise ParsingError.new(obj.errors.full_messages.join('\n')) unless obj.valid? + end + + def group_from_entry(entry) + entry[:name] = entry.delete(:group) + entry[:metrics]&.map! do |entry| + Metric.new(entry).tap(&method(:validate!)) + end + + MetricGroup.new(entry).tap(&method(:validate!)) + end + + def additional_metrics_raw + load_yaml_file&.map(&:deep_symbolize_keys).freeze + end + + def load_yaml_file + @loaded_yaml_file ||= YAML.load_file(Rails.root.join('config/prometheus/additional_metrics.yml')) + end + end + end +end diff --git a/lib/gitlab/prometheus/metric.rb b/lib/gitlab/prometheus/metric.rb new file mode 100644 index 00000000000..f54b2c6aaff --- /dev/null +++ b/lib/gitlab/prometheus/metric.rb @@ -0,0 +1,16 @@ +module Gitlab + module Prometheus + class Metric + include ActiveModel::Model + + attr_accessor :title, :required_metrics, :weight, :y_label, :queries + + validates :title, :required_metrics, :weight, :y_label, :queries, presence: true + + def initialize(params = {}) + super(params) + @y_label ||= 'Values' + end + end + end +end diff --git a/lib/gitlab/prometheus/metric_group.rb b/lib/gitlab/prometheus/metric_group.rb new file mode 100644 index 00000000000..729fef34b35 --- /dev/null +++ b/lib/gitlab/prometheus/metric_group.rb @@ -0,0 +1,14 @@ +module Gitlab + module Prometheus + class MetricGroup + include ActiveModel::Model + + attr_accessor :name, :priority, :metrics + validates :name, :priority, :metrics, presence: true + + def self.all + AdditionalMetricsParser.load_groups_from_yaml + end + end + end +end diff --git a/lib/gitlab/prometheus/parsing_error.rb b/lib/gitlab/prometheus/parsing_error.rb new file mode 100644 index 00000000000..49cc0e16080 --- /dev/null +++ b/lib/gitlab/prometheus/parsing_error.rb @@ -0,0 +1,5 @@ +module Gitlab + module Prometheus + ParsingError = Class.new(StandardError) + end +end diff --git a/lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb b/lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb new file mode 100644 index 00000000000..67c69d9ccf3 --- /dev/null +++ b/lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb @@ -0,0 +1,22 @@ +module Gitlab + module Prometheus + module Queries + class AdditionalMetricsDeploymentQuery < BaseQuery + include QueryAdditionalMetrics + + def query(deployment_id) + Deployment.find_by(id: deployment_id).try do |deployment| + query_context = { + environment_slug: deployment.environment.slug, + environment_filter: %{container_name!="POD",environment="#{deployment.environment.slug}"}, + timeframe_start: (deployment.created_at - 30.minutes).to_f, + timeframe_end: (deployment.created_at + 30.minutes).to_f + } + + query_metrics(query_context) + end + end + end + end + end +end diff --git a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb new file mode 100644 index 00000000000..b5a679ddd79 --- /dev/null +++ b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb @@ -0,0 +1,22 @@ +module Gitlab + module Prometheus + module Queries + class AdditionalMetricsEnvironmentQuery < BaseQuery + include QueryAdditionalMetrics + + def query(environment_id) + Environment.find_by(id: environment_id).try do |environment| + query_context = { + environment_slug: environment.slug, + environment_filter: %{container_name!="POD",environment="#{environment.slug}"}, + timeframe_start: 8.hours.ago.to_f, + timeframe_end: Time.now.to_f + } + + query_metrics(query_context) + end + end + end + end + end +end diff --git a/lib/gitlab/prometheus/queries/base_query.rb b/lib/gitlab/prometheus/queries/base_query.rb index 2a2eb4ae57f..c60828165bd 100644 --- a/lib/gitlab/prometheus/queries/base_query.rb +++ b/lib/gitlab/prometheus/queries/base_query.rb @@ -3,7 +3,7 @@ module Gitlab module Queries class BaseQuery attr_accessor :client - delegate :query_range, :query, to: :client, prefix: true + delegate :query_range, :query, :label_values, :series, to: :client, prefix: true def raw_memory_usage_query(environment_slug) %{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20} diff --git a/lib/gitlab/prometheus/queries/deployment_query.rb b/lib/gitlab/prometheus/queries/deployment_query.rb index 2cc08731f8d..170f483540e 100644 --- a/lib/gitlab/prometheus/queries/deployment_query.rb +++ b/lib/gitlab/prometheus/queries/deployment_query.rb @@ -1,26 +1,31 @@ -module Gitlab::Prometheus::Queries - class DeploymentQuery < BaseQuery - def query(deployment_id) - deployment = Deployment.find_by(id: deployment_id) - environment_slug = deployment.environment.slug +module Gitlab + module Prometheus + module Queries + class DeploymentQuery < BaseQuery + def query(deployment_id) + Deployment.find_by(id: deployment_id).try do |deployment| + environment_slug = deployment.environment.slug - memory_query = raw_memory_usage_query(environment_slug) - memory_avg_query = %{avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}[30m]))} - cpu_query = raw_cpu_usage_query(environment_slug) - cpu_avg_query = %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[30m])) * 100} + memory_query = raw_memory_usage_query(environment_slug) + memory_avg_query = %{avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}[30m]))} + cpu_query = raw_cpu_usage_query(environment_slug) + cpu_avg_query = %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[30m])) * 100} - timeframe_start = (deployment.created_at - 30.minutes).to_f - timeframe_end = (deployment.created_at + 30.minutes).to_f + timeframe_start = (deployment.created_at - 30.minutes).to_f + timeframe_end = (deployment.created_at + 30.minutes).to_f - { - memory_values: client_query_range(memory_query, start: timeframe_start, stop: timeframe_end), - memory_before: client_query(memory_avg_query, time: deployment.created_at.to_f), - memory_after: client_query(memory_avg_query, time: timeframe_end), + { + memory_values: client_query_range(memory_query, start: timeframe_start, stop: timeframe_end), + memory_before: client_query(memory_avg_query, time: deployment.created_at.to_f), + memory_after: client_query(memory_avg_query, time: timeframe_end), - cpu_values: client_query_range(cpu_query, start: timeframe_start, stop: timeframe_end), - cpu_before: client_query(cpu_avg_query, time: deployment.created_at.to_f), - cpu_after: client_query(cpu_avg_query, time: timeframe_end) - } + cpu_values: client_query_range(cpu_query, start: timeframe_start, stop: timeframe_end), + cpu_before: client_query(cpu_avg_query, time: deployment.created_at.to_f), + cpu_after: client_query(cpu_avg_query, time: timeframe_end) + } + end + end + end end end end diff --git a/lib/gitlab/prometheus/queries/environment_query.rb b/lib/gitlab/prometheus/queries/environment_query.rb index 01d756d7284..66f29d95177 100644 --- a/lib/gitlab/prometheus/queries/environment_query.rb +++ b/lib/gitlab/prometheus/queries/environment_query.rb @@ -1,20 +1,25 @@ -module Gitlab::Prometheus::Queries - class EnvironmentQuery < BaseQuery - def query(environment_id) - environment = Environment.find_by(id: environment_id) - environment_slug = environment.slug - timeframe_start = 8.hours.ago.to_f - timeframe_end = Time.now.to_f +module Gitlab + module Prometheus + module Queries + class EnvironmentQuery < BaseQuery + def query(environment_id) + Environment.find_by(id: environment_id).try do |environment| + environment_slug = environment.slug + timeframe_start = 8.hours.ago.to_f + timeframe_end = Time.now.to_f - memory_query = raw_memory_usage_query(environment_slug) - cpu_query = raw_cpu_usage_query(environment_slug) + memory_query = raw_memory_usage_query(environment_slug) + cpu_query = raw_cpu_usage_query(environment_slug) - { - memory_values: client_query_range(memory_query, start: timeframe_start, stop: timeframe_end), - memory_current: client_query(memory_query, time: timeframe_end), - cpu_values: client_query_range(cpu_query, start: timeframe_start, stop: timeframe_end), - cpu_current: client_query(cpu_query, time: timeframe_end) - } + { + memory_values: client_query_range(memory_query, start: timeframe_start, stop: timeframe_end), + memory_current: client_query(memory_query, time: timeframe_end), + cpu_values: client_query_range(cpu_query, start: timeframe_start, stop: timeframe_end), + cpu_current: client_query(cpu_query, time: timeframe_end) + } + end + end + end end end end diff --git a/lib/gitlab/prometheus/queries/matched_metrics_query.rb b/lib/gitlab/prometheus/queries/matched_metrics_query.rb new file mode 100644 index 00000000000..d4894c87f8d --- /dev/null +++ b/lib/gitlab/prometheus/queries/matched_metrics_query.rb @@ -0,0 +1,80 @@ +module Gitlab + module Prometheus + module Queries + class MatchedMetricsQuery < BaseQuery + MAX_QUERY_ITEMS = 40.freeze + + def query + groups_data.map do |group, data| + { + group: group.name, + priority: group.priority, + active_metrics: data[:active_metrics], + metrics_missing_requirements: data[:metrics_missing_requirements] + } + end + end + + private + + def groups_data + metrics_groups = groups_with_active_metrics(Gitlab::Prometheus::MetricGroup.all) + lookup = active_series_lookup(metrics_groups) + + groups = {} + + metrics_groups.each do |group| + groups[group] ||= { active_metrics: 0, metrics_missing_requirements: 0 } + active_metrics = group.metrics.count { |metric| metric.required_metrics.all?(&lookup.method(:has_key?)) } + + groups[group][:active_metrics] += active_metrics + groups[group][:metrics_missing_requirements] += group.metrics.count - active_metrics + end + + groups + end + + def active_series_lookup(metric_groups) + timeframe_start = 8.hours.ago + timeframe_end = Time.now + + series = metric_groups.flat_map(&:metrics).flat_map(&:required_metrics).uniq + + lookup = series.each_slice(MAX_QUERY_ITEMS).flat_map do |batched_series| + client_series(*batched_series, start: timeframe_start, stop: timeframe_end) + .select(&method(:has_matching_label)) + .map { |series_info| [series_info['__name__'], true] } + end + lookup.to_h + end + + def has_matching_label(series_info) + series_info.key?('environment') + end + + def available_metrics + @available_metrics ||= client_label_values || [] + end + + def filter_active_metrics(metric_group) + metric_group.metrics.select! do |metric| + metric.required_metrics.all?(&available_metrics.method(:include?)) + end + metric_group + end + + def groups_with_active_metrics(metric_groups) + metric_groups.map(&method(:filter_active_metrics)).select { |group| group.metrics.any? } + end + + def metrics_with_required_series(metric_groups) + metric_groups.flat_map do |group| + group.metrics.select do |metric| + metric.required_metrics.all?(&available_metrics.method(:include?)) + end + end + end + end + end + end +end diff --git a/lib/gitlab/prometheus/queries/query_additional_metrics.rb b/lib/gitlab/prometheus/queries/query_additional_metrics.rb new file mode 100644 index 00000000000..e44be770544 --- /dev/null +++ b/lib/gitlab/prometheus/queries/query_additional_metrics.rb @@ -0,0 +1,73 @@ +module Gitlab + module Prometheus + module Queries + module QueryAdditionalMetrics + def query_metrics(query_context) + query_processor = method(:process_query).curry[query_context] + + groups = matched_metrics.map do |group| + metrics = group.metrics.map do |metric| + { + title: metric.title, + weight: metric.weight, + y_label: metric.y_label, + queries: metric.queries.map(&query_processor).select(&method(:query_with_result)) + } + end + + { + group: group.name, + priority: group.priority, + metrics: metrics.select(&method(:metric_with_any_queries)) + } + end + + groups.select(&method(:group_with_any_metrics)) + end + + private + + def metric_with_any_queries(metric) + metric[:queries]&.count&.> 0 + end + + def group_with_any_metrics(group) + group[:metrics]&.count&.> 0 + end + + def query_with_result(query) + query[:result]&.any? do |item| + item&.[](:values)&.any? || item&.[](:value)&.any? + end + end + + def process_query(context, query) + query_with_result = query.dup + result = + if query.key?(:query_range) + client_query_range(query[:query_range] % context, start: context[:timeframe_start], stop: context[:timeframe_end]) + else + client_query(query[:query] % context, time: context[:timeframe_end]) + end + query_with_result[:result] = result&.map(&:deep_symbolize_keys) + query_with_result + end + + def available_metrics + @available_metrics ||= client_label_values || [] + end + + def matched_metrics + result = Gitlab::Prometheus::MetricGroup.all.map do |group| + group.metrics.select! do |metric| + metric.required_metrics.all?(&available_metrics.method(:include?)) + end + group + end + + result.select { |group| group.metrics.any? } + end + end + end + end +end diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb index 5b51a1779dd..aa94614bf18 100644 --- a/lib/gitlab/prometheus_client.rb +++ b/lib/gitlab/prometheus_client.rb @@ -29,6 +29,14 @@ module Gitlab end end + def label_values(name = '__name__') + json_api_get("label/#{name}/values") + end + + def series(*matches, start: 8.hours.ago, stop: Time.now) + json_api_get('series', 'match': matches, start: start.to_f, end: stop.to_f) + end + private def json_api_get(type, args = {}) diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/quick_actions/command_definition.rb index caab8856014..3937d9c153a 100644 --- a/lib/gitlab/slash_commands/command_definition.rb +++ b/lib/gitlab/quick_actions/command_definition.rb @@ -1,5 +1,5 @@ module Gitlab - module SlashCommands + module QuickActions class CommandDefinition attr_accessor :name, :aliases, :description, :explanation, :params, :condition_block, :parse_params_block, :action_block diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/quick_actions/dsl.rb index 1b5b4566d81..a4a97236ffc 100644 --- a/lib/gitlab/slash_commands/dsl.rb +++ b/lib/gitlab/quick_actions/dsl.rb @@ -1,5 +1,5 @@ module Gitlab - module SlashCommands + module QuickActions module Dsl extend ActiveSupport::Concern @@ -14,7 +14,7 @@ module Gitlab end class_methods do - # Allows to give a description to the next slash command. + # Allows to give a description to the next quick action. # This description is shown in the autocomplete menu. # It accepts a block that will be evaluated with the context given to # `CommandDefintion#to_h`. @@ -31,7 +31,7 @@ module Gitlab @description = block_given? ? block : text end - # Allows to define params for the next slash command. + # Allows to define params for the next quick action. # These params are shown in the autocomplete menu. # # Example: diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/quick_actions/extractor.rb index 6dbb467d70d..09576be7156 100644 --- a/lib/gitlab/slash_commands/extractor.rb +++ b/lib/gitlab/quick_actions/extractor.rb @@ -1,10 +1,10 @@ module Gitlab - module SlashCommands + module QuickActions # This class takes an array of commands that should be extracted from a # given text. # # ``` - # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels]) + # extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels]) # ``` class Extractor attr_reader :command_definitions @@ -24,7 +24,7 @@ module Gitlab # # Usage: # ``` - # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels]) + # extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels]) # msg = %(hello\n/labels ~foo ~"bar baz"\nworld) # commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']] # msg #=> "hello\nworld" diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index e4d2a992470..b706434217d 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -43,7 +43,7 @@ module Gitlab end def environment_name_regex_message - "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces" + "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces" end def kubernetes_namespace_regex diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 878e03f61d7..3591fa9145e 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -3,16 +3,18 @@ module Gitlab NotFoundError = Class.new(StandardError) def self.parse(repo_path) + wiki = false project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false) - project = Project.find_by_full_path(project_path) - if project_path.end_with?('.wiki') && !project - project = Project.find_by_full_path(project_path.chomp('.wiki')) + project, was_redirected = find_project(project_path) + + if project_path.end_with?('.wiki') && project.nil? + project, was_redirected = find_project(project_path.chomp('.wiki')) wiki = true - else - wiki = false end - [project, wiki] + redirected_path = project_path if was_redirected + + [project, wiki, redirected_path] end def self.strip_storage_path(repo_path, fail_on_not_found: true) @@ -30,5 +32,12 @@ module Gitlab result.sub(/\A\/*/, '') end + + def self.find_project(project_path) + project = Project.find_by_full_path(project_path, follow_redirects: true) + was_redirected = project && project.full_path.casecmp(project_path) != 0 + + [project, was_redirected] + end end end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index b1d6ea665b7..22554236c38 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -30,8 +30,8 @@ module Gitlab end def version_required - @version_required ||= File.read(Rails.root. - join('GITLAB_SHELL_VERSION')).strip + @version_required ||= File.read(Rails.root + .join('GITLAB_SHELL_VERSION')).strip end def strip_key(key) diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb index aa1468bff6b..b5f9d040047 100644 --- a/lib/gitlab/sherlock/line_profiler.rb +++ b/lib/gitlab/sherlock/line_profiler.rb @@ -77,8 +77,8 @@ module Gitlab line_samples << LineSample.new(duration, events) end - samples << FileSample. - new(file, line_samples, total_duration, total_events) + samples << FileSample + .new(file, line_samples, total_duration, total_events) end samples diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb index 99e56e923eb..948bf5e6528 100644 --- a/lib/gitlab/sherlock/query.rb +++ b/lib/gitlab/sherlock/query.rb @@ -105,10 +105,10 @@ module Gitlab end def format_sql(query) - query.each_line. - map { |line| line.strip }. - join("\n"). - gsub(PREFIX_NEWLINE) { "\n#{$1} " } + query.each_line + .map { |line| line.strip } + .join("\n") + .gsub(PREFIX_NEWLINE) { "\n#{$1} " } end end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/slash_commands/base_command.rb index 25da8474e95..cc3c9a50555 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/slash_commands/base_command.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands class BaseCommand QUERY_LIMIT = 5 diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/slash_commands/command.rb index 3e0c30c33b7..a78408b0519 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -1,11 +1,11 @@ module Gitlab - module ChatCommands + module SlashCommands class Command < BaseCommand COMMANDS = [ - Gitlab::ChatCommands::IssueShow, - Gitlab::ChatCommands::IssueNew, - Gitlab::ChatCommands::IssueSearch, - Gitlab::ChatCommands::Deploy + Gitlab::SlashCommands::IssueShow, + Gitlab::SlashCommands::IssueNew, + Gitlab::SlashCommands::IssueSearch, + Gitlab::SlashCommands::Deploy ].freeze def execute @@ -15,10 +15,10 @@ module Gitlab if command.allowed?(project, current_user) command.new(project, current_user, params).execute(match) else - Gitlab::ChatCommands::Presenters::Access.new.access_denied + Gitlab::SlashCommands::Presenters::Access.new.access_denied end else - Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text]) + Gitlab::SlashCommands::Help.new(project, current_user, params).execute(available_commands, params[:text]) end end diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/slash_commands/deploy.rb index 458d90f84e8..e71eb15d604 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/slash_commands/deploy.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands class Deploy < BaseCommand def self.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) @@ -24,12 +24,12 @@ module Gitlab actions = find_actions(from, to) if actions.none? - Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions + Gitlab::SlashCommands::Presenters::Deploy.new(nil).no_actions elsif actions.one? action = play!(from, to, actions.first) - Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to) + Gitlab::SlashCommands::Presenters::Deploy.new(action).present(from, to) else - Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions + Gitlab::SlashCommands::Presenters::Deploy.new(actions).too_many_actions end end diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/slash_commands/help.rb index 6c0e4d304a4..81f3707e03e 100644 --- a/lib/gitlab/chat_commands/help.rb +++ b/lib/gitlab/slash_commands/help.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands 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 @@ -17,7 +17,7 @@ module Gitlab end def execute(commands, text) - Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text) + Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, text) end def trigger diff --git a/lib/gitlab/chat_commands/issue_command.rb b/lib/gitlab/slash_commands/issue_command.rb index 84de3e44c70..87ea19b8806 100644 --- a/lib/gitlab/chat_commands/issue_command.rb +++ b/lib/gitlab/slash_commands/issue_command.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands class IssueCommand < BaseCommand def self.available?(project) project.issues_enabled? && project.default_issues_tracker? diff --git a/lib/gitlab/chat_commands/issue_new.rb b/lib/gitlab/slash_commands/issue_new.rb index 016054ecd46..25f965e843d 100644 --- a/lib/gitlab/chat_commands/issue_new.rb +++ b/lib/gitlab/slash_commands/issue_new.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands class IssueNew < IssueCommand def self.match(text) # we can not match \n with the dot by passing the m modifier as than @@ -35,7 +35,7 @@ module Gitlab end def presenter(issue) - Gitlab::ChatCommands::Presenters::IssueNew.new(issue) + Gitlab::SlashCommands::Presenters::IssueNew.new(issue) end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/slash_commands/issue_search.rb index 3491b53093e..acba84b54b4 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/slash_commands/issue_search.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands class IssueSearch < IssueCommand def self.match(text) /\Aissue\s+search\s+(?<query>.*)/.match(text) diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/slash_commands/issue_show.rb index d6013f4d10c..ffa5184e5cb 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/slash_commands/issue_show.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands class IssueShow < IssueCommand def self.match(text) /\Aissue\s+show\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text) @@ -13,9 +13,9 @@ module Gitlab issue = find_by_iid(match[:iid]) if issue - Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present + Gitlab::SlashCommands::Presenters::IssueShow.new(issue).present else - Gitlab::ChatCommands::Presenters::Access.new.not_found + Gitlab::SlashCommands::Presenters::Access.new.not_found end end end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb index 92f4fa17f78..1a817eb735b 100644 --- a/lib/gitlab/chat_commands/presenters/access.rb +++ b/lib/gitlab/slash_commands/presenters/access.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters class Access < Presenters::Base def access_denied diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/slash_commands/presenters/base.rb index 05994bee79d..27696436574 100644 --- a/lib/gitlab/chat_commands/presenters/base.rb +++ b/lib/gitlab/slash_commands/presenters/base.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters class Base include Gitlab::Routing.url_helpers diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/slash_commands/presenters/deploy.rb index 863d0bf99ca..b8dc77bd37b 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/slash_commands/presenters/deploy.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters class Deploy < Presenters::Base def present(from, to) diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/slash_commands/presenters/help.rb index cd47b7f4c6a..ea611a4d629 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/slash_commands/presenters/help.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters class Help < Presenters::Base def present(trigger, text) diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb index 25bc82994ba..341f2aabdd0 100644 --- a/lib/gitlab/chat_commands/presenters/issue_base.rb +++ b/lib/gitlab/slash_commands/presenters/issue_base.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters module IssueBase def color(issuable) diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/slash_commands/presenters/issue_new.rb index 3674ba25641..86490a39cc1 100644 --- a/lib/gitlab/chat_commands/presenters/issue_new.rb +++ b/lib/gitlab/slash_commands/presenters/issue_new.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters class IssueNew < Presenters::Base include Presenters::IssueBase diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/slash_commands/presenters/issue_search.rb index 73788cf9662..4e27d668685 100644 --- a/lib/gitlab/chat_commands/presenters/issue_search.rb +++ b/lib/gitlab/slash_commands/presenters/issue_search.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters class IssueSearch < Presenters::Base include Presenters::IssueBase diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/slash_commands/presenters/issue_show.rb index bd784ad241e..c99316df667 100644 --- a/lib/gitlab/chat_commands/presenters/issue_show.rb +++ b/lib/gitlab/slash_commands/presenters/issue_show.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands module Presenters class IssueShow < Presenters::Base include Presenters::IssueBase diff --git a/lib/gitlab/chat_commands/result.rb b/lib/gitlab/slash_commands/result.rb index 324d7ef43a3..7021b4b01b2 100644 --- a/lib/gitlab/chat_commands/result.rb +++ b/lib/gitlab/slash_commands/result.rb @@ -1,5 +1,5 @@ module Gitlab - module ChatCommands + module SlashCommands Result = Struct.new(:type, :message) end end diff --git a/lib/gitlab/sql/recursive_cte.rb b/lib/gitlab/sql/recursive_cte.rb index 5b1b03820a3..16ec002f139 100644 --- a/lib/gitlab/sql/recursive_cte.rb +++ b/lib/gitlab/sql/recursive_cte.rb @@ -52,10 +52,10 @@ module Gitlab # Applies the CTE to the given relation, returning a new one that will # query from it. def apply_to(relation) - relation.except(:where). - with. - recursive(to_arel). - from(alias_to(relation.model.arel_table)) + relation.except(:where) + .with + .recursive(to_arel) + .from(alias_to(relation.model.arel_table)) end end end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index bcba2e3e1b6..38dc82493cf 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -27,6 +27,7 @@ module Gitlab deploy_keys: DeployKey.count, deployments: Deployment.count, environments: Environment.count, + in_review_folder: Environment.in_review_folder.count, groups: Group.count, issues: Issue.count, keys: Key.count, diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 2b53798e70f..48f3d950779 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -13,18 +13,8 @@ module Gitlab scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } - scope :public_to_user, -> (user) do - if user - if user.admin? - all - elsif !user.external? - public_and_internal_only - else - public_only - end - else - public_only - end + scope :public_to_user, -> (user = nil) do + where(visibility_level: VisibilityLevel.levels_for_user(user)) end end @@ -35,6 +25,18 @@ module Gitlab class << self delegate :values, to: :options + def levels_for_user(user = nil) + return [PUBLIC] unless user + + if user.full_private_access? + [PRIVATE, INTERNAL, PUBLIC] + elsif user.external? + [PUBLIC] + else + [INTERNAL, PUBLIC] + end + end + def string_values string_options.keys end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 7f27317775c..f96ee69096d 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -26,7 +26,10 @@ module Gitlab } if Gitlab.config.gitaly.enabled - address = Gitlab::GitalyClient.address(project.repository_storage) + server = { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + } params[:Repository] = repository.gitaly_repository.to_h feature_enabled = case action.to_s @@ -39,8 +42,10 @@ module Gitlab else raise "Unsupported action: #{action}" end - - params[:GitalyAddress] = address if feature_enabled + if feature_enabled + params[:GitalyAddress] = server[:address] # This field will be deprecated + params[:GitalyServer] = server + end end params diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb index dc2d4643a01..e5986612908 100644 --- a/lib/system_check/simple_executor.rb +++ b/lib/system_check/simple_executor.rb @@ -75,6 +75,8 @@ module SystemCheck check.show_error end + rescue StandardError => e + $stdout.puts "Exception: #{e.message}".color(:red) end private diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 3c5bc0146a1..a8db5701d0b 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -30,11 +30,7 @@ namespace :gitlab do puts "# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}" puts "# This is in TOML format suitable for use in Gitaly's config.toml file." - config = Gitlab.config.repositories.storages.map do |key, val| - { name: key, path: val['path'] } - end - - puts TOML.dump(storage: config) + puts gitaly_configuration_toml end private @@ -42,10 +38,10 @@ namespace :gitlab do # We cannot create config.toml files for all possible Gitaly configuations. # For instance, if Gitaly is running on another machine then it makes no # sense to write a config.toml file on the current machine. This method will - # only write a config.toml file in the most common and simplest case: the - # case where we have exactly one Gitaly process and we are sure it is - # running locally because it uses a Unix socket. - def create_gitaly_configuration + # only generate a configuration for the most common and simplest case: when + # we have exactly one Gitaly process and we are sure it is running locally + # because it uses a Unix socket. + def gitaly_configuration_toml storages = [] address = nil @@ -62,9 +58,14 @@ namespace :gitlab do storages << { name: key, path: val['path'] } end + config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } + config[:auth] = { token: 'secret' } if Rails.env.test? + TOML.dump(config) + end + def create_gitaly_configuration File.open("config.toml", "w") do |f| - f.puts TOML.dump(socket_path: address.sub(%r{\Aunix:}, ''), storages: storages) + f.puts gitaly_configuration_toml end rescue ArgumentError => e puts "Skipping config.toml generation:" diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake index 761f275d42a..151f42a2222 100644 --- a/lib/tasks/migrate/add_limits_mysql.rake +++ b/lib/tasks/migrate/add_limits_mysql.rake @@ -1,9 +1,11 @@ require Rails.root.join('db/migrate/limits_to_mysql') require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql') +require Rails.root.join('db/migrate/merge_request_diff_file_limits_to_mysql') desc "GitLab | Add limits to strings in mysql database" task add_limits_mysql: :environment do puts "Adding limits to schema.rb for mysql" LimitsToMysql.new.up MarkdownCacheLimitsToMysql.new.up + MergeRequestDiffFileLimitsToMysql.new.up end diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po index e6caf83252d..370aca1f1d9 100644 --- a/locale/bg/gitlab.po +++ b/locale/bg/gitlab.po @@ -3,25 +3,245 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-05-04 19:24-0500\n" +"POT-Creation-Date: 2017-06-12 19:29-0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-06-05 09:40-0400\n" +"PO-Revision-Date: 2017-06-13 04:23-0400\n" "Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n" -"Language-Team: Bulgarian\n" +"Language-Team: Bulgarian (https://translate.zanata.org/project/view/GitLab)\n" "Language: bg\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "%{commit_author_link} подаде %{commit_timeago}" + +msgid "About auto deploy" +msgstr "Относно автоматичното внедряване" + +msgid "Active" +msgstr "Активно" + +msgid "Activity" +msgstr "Дейност" + +msgid "Add Changelog" +msgstr "Добавяне на списък с промени" + +msgid "Add Contribution guide" +msgstr "Добавяне на ръководство за сътрудничество" + +msgid "Add License" +msgstr "Добавяне на лиценз" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "" +"Добавете SSH ключ в профила си, за да можете да изтегляте или изпращате " +"промени чрез SSH." + +msgid "Add new directory" +msgstr "Добавяне на нова папка" + +msgid "Archived project! Repository is read-only" +msgstr "Архивиран проект! Хранилището е само за четене" + +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "Наистина ли искате да изтриете този план за схема?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "Прикачете файл чрез влачене и пускане или %{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "Клон" +msgstr[1] "Клонове" + +msgid "" +"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" +msgstr "" +"Клонът <strong>%{branch_name}</strong> беше създаден. За да настроите " +"автоматичното внедряване, изберете Yaml шаблон за GitLab CI и подайте " +"промените си. %{link_to_autodeploy_doc}" + +msgid "Branches" +msgstr "Клонове" + +msgid "Browse files" +msgstr "Разглеждане на файловете" + msgid "ByAuthor|by" msgstr "от" +msgid "CI configuration" +msgstr "Конфигурация на непрекъсната интеграция" + +msgid "Cancel" +msgstr "Отказ" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "Избиране в клона" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "Отмяна в клона" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "Подбиране" + +msgid "ChangeType|commit" +msgstr "подаване" + +msgid "ChangeType|merge request" +msgstr "заявка за сливане" + +msgid "Changelog" +msgstr "Списък с промени" + +msgid "Charts" +msgstr "Графики" + +msgid "Cherry-pick this commit" +msgstr "Подбиране на това подаване" + +msgid "Cherry-pick this merge-request" +msgstr "Подбиране на тази заявка за сливане" + +msgid "CiStatusLabel|canceled" +msgstr "отказано" + +msgid "CiStatusLabel|created" +msgstr "създадено" + +msgid "CiStatusLabel|failed" +msgstr "неуспешно" + +msgid "CiStatusLabel|manual action" +msgstr "ръчно действие" + +msgid "CiStatusLabel|passed" +msgstr "успешно" + +msgid "CiStatusLabel|passed with warnings" +msgstr "успешно, с предупреждения" + +msgid "CiStatusLabel|pending" +msgstr "на изчакване" + +msgid "CiStatusLabel|skipped" +msgstr "пропуснато" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "чакане за ръчно действие" + +msgid "CiStatusText|blocked" +msgstr "блокирано" + +msgid "CiStatusText|canceled" +msgstr "отказано" + +msgid "CiStatusText|created" +msgstr "създадено" + +msgid "CiStatusText|failed" +msgstr "неуспешно" + +msgid "CiStatusText|manual" +msgstr "ръчно" + +msgid "CiStatusText|passed" +msgstr "успешно" + +msgid "CiStatusText|pending" +msgstr "на изчакване" + +msgid "CiStatusText|skipped" +msgstr "пропуснато" + +msgid "CiStatus|running" +msgstr "протича в момента" + msgid "Commit" msgid_plural "Commits" msgstr[0] "Подаване" msgstr[1] "Подавания" +msgid "Commit message" +msgstr "Съобщение за подаването" + +msgid "CommitMessage|Add %{file_name}" +msgstr "Добавяне на „%{file_name}“" + +msgid "Commits" +msgstr "Подавания" + +msgid "Commits|History" +msgstr "История" + +msgid "Committed by" +msgstr "Подадено от" + +msgid "Compare" +msgstr "Сравнение" + +msgid "Contribution guide" +msgstr "Ръководство за сътрудничество" + +msgid "Contributors" +msgstr "Сътрудници" + +msgid "Copy URL to clipboard" +msgstr "Копиране на адреса в буфера за обмен" + +msgid "Copy commit SHA to clipboard" +msgstr "Копиране на идентификатора на подаването в буфера за обмен" + +msgid "Create New Directory" +msgstr "Създаване на нова папка" + +msgid "Create directory" +msgstr "Създаване на папка" + +msgid "Create empty bare repository" +msgstr "Създаване на празно хранилище" + +msgid "Create merge request" +msgstr "Създаване на заявка за сливане" + +msgid "Create new..." +msgstr "Създаване на нов…" + +msgid "CreateNewFork|Fork" +msgstr "Разклоняване" + +msgid "CreateTag|Tag" +msgstr "Етикет" + +msgid "Cron Timezone" +msgstr "Часова зона за „Cron“" + +msgid "Cron syntax" +msgstr "Синтаксис на „Cron“" + +msgid "Custom" +msgstr "Персонализиран" + +msgid "Custom notification events" +msgstr "Персонализирани събития за известяване" + +msgid "" +"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 %{notification_link}." +msgstr "" +"Персонализираните нива на известяване са същите като нивата за участие. С " +"персонализираните нива на известяване ще можете да получавате и известия за " +"избрани събития. За да научите повече, прегледайте %{notification_link}." + +msgid "Cycle Analytics" +msgstr "Анализ на циклите" + msgid "" "Cycle Analytics gives an overview of how much time it takes to go from idea " "to production in your project." @@ -50,17 +270,97 @@ msgstr "Подготовка за издаване" msgid "CycleAnalyticsStage|Test" msgstr "Тестване" +msgid "Define a custom pattern with cron syntax" +msgstr "Задайте потребителски шаблон, използвайки синтаксиса на „Cron“" + +msgid "Delete" +msgstr "Изтриване" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "Внедряване" msgstr[1] "Внедрявания" +msgid "Description" +msgstr "Описание" + +msgid "Directory name" +msgstr "Име на папката" + +msgid "Don't show again" +msgstr "Да не се показва повече" + +msgid "Download" +msgstr "Сваляне" + +msgid "Download tar" +msgstr "Сваляне във формат „tar“" + +msgid "Download tar.bz2" +msgstr "Сваляне във формат „tar.bz2“" + +msgid "Download tar.gz" +msgstr "Сваляне във формат „tar.gz“" + +msgid "Download zip" +msgstr "Сваляне във формат „zip“" + +msgid "DownloadArtifacts|Download" +msgstr "Сваляне" + +msgid "DownloadCommit|Email Patches" +msgstr "Изпращане на кръпките по е-поща" + +msgid "DownloadCommit|Plain Diff" +msgstr "Обикновен файл с разлики" + +msgid "DownloadSource|Download" +msgstr "Сваляне" + +msgid "Edit" +msgstr "Редактиране" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "Редактиране на плана %{id} за схема" + +msgid "Every day (at 4:00am)" +msgstr "Всеки ден (в 4 ч. сутринта)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "Всеки месец (на 1-во число, в 4 ч. сутринта)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "Всяка седмица (в неделя, в 4 ч. сутринта)" + +msgid "Failed to change the owner" +msgstr "Собственикът не може да бъде променен" + +msgid "Failed to remove the pipeline schedule" +msgstr "Планът за схема не може да бъде премахнат" + +msgid "Files" +msgstr "Файлове" + +msgid "Find by path" +msgstr "Търсене по път" + +msgid "Find file" +msgstr "Търсене на файл" + msgid "FirstPushedBy|First" msgstr "Първо" msgid "FirstPushedBy|pushed by" msgstr "изпращане на промени от" +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "Разклонение" +msgstr[1] "Разклонения" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "Разклонение на" + msgid "From issue creation until deploy to production" msgstr "От създаването на проблема до внедряването в крайната версия" @@ -68,50 +368,290 @@ msgid "From merge request merge until deploy to production" msgstr "" "От прилагането на заявката за сливане до внедряването в крайната версия" +msgid "Go to your fork" +msgstr "Към Вашето разклонение" + +msgid "GoToYourFork|Fork" +msgstr "Разклонение" + +msgid "Home" +msgstr "Начало" + +msgid "Housekeeping successfully started" +msgstr "Освежаването започна успешно" + +msgid "Import repository" +msgstr "Внасяне на хранилище" + +msgid "Interval Pattern" +msgstr "Шаблон за интервала" + msgid "Introducing Cycle Analytics" -msgstr "Представяме Ви анализът на циклите" +msgstr "Представяме Ви анализа на циклите" + +msgid "LFSStatus|Disabled" +msgstr "Изключено" + +msgid "LFSStatus|Enabled" +msgstr "Включено" msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "Последния %d ден" msgstr[1] "Последните %d дни" +msgid "Last Pipeline" +msgstr "Последна схема" + +msgid "Last Update" +msgstr "Последна промяна" + +msgid "Last commit" +msgstr "Последно подаване" + +msgid "Learn more in the" +msgstr "Научете повече в" + +msgid "Leave group" +msgstr "Напускане на групата" + +msgid "Leave project" +msgstr "Напускане на проекта" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" -msgstr[0] "Ограничено до показване на последното %d събитие" -msgstr[1] "Ограничено до показване на последните %d събития" +msgstr[0] "Ограничено до показване на най-много %d събитие" +msgstr[1] "Ограничено до показване на най-много %d събития" msgid "Median" msgstr "Медиана" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "добавите SSH ключ" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "Нов проблем" msgstr[1] "Нови проблема" +msgid "New Pipeline Schedule" +msgstr "Нов план за схема" + +msgid "New branch" +msgstr "Нов клон" + +msgid "New directory" +msgstr "Нова папка" + +msgid "New file" +msgstr "Нов файл" + +msgid "New issue" +msgstr "Нов проблем" + +msgid "New merge request" +msgstr "Нова заявка за сливане" + +msgid "New schedule" +msgstr "Нов план" + +msgid "New snippet" +msgstr "Нов отрязък" + +msgid "New tag" +msgstr "Нов етикет" + +msgid "No repository" +msgstr "Няма хранилище" + +msgid "No schedules" +msgstr "Няма планове" + msgid "Not available" msgstr "Не е налично" msgid "Not enough data" msgstr "Няма достатъчно данни" +msgid "Notification events" +msgstr "Събития за известяване" + +msgid "NotificationEvent|Close issue" +msgstr "Затваряне на проблем" + +msgid "NotificationEvent|Close merge request" +msgstr "Затваряне на заявка за сливане" + +msgid "NotificationEvent|Failed pipeline" +msgstr "Неуспешно изпълнение на схема" + +msgid "NotificationEvent|Merge merge request" +msgstr "Прилагане на заявка за сливане" + +msgid "NotificationEvent|New issue" +msgstr "Нов проблем" + +msgid "NotificationEvent|New merge request" +msgstr "Нова заявка за сливане" + +msgid "NotificationEvent|New note" +msgstr "Нова бележка" + +msgid "NotificationEvent|Reassign issue" +msgstr "Преназначаване на проблем" + +msgid "NotificationEvent|Reassign merge request" +msgstr "Преназначаване на заявка за сливане" + +msgid "NotificationEvent|Reopen issue" +msgstr "Повторно отваряне на проблем" + +msgid "NotificationEvent|Successful pipeline" +msgstr "Успешно изпълнение на схема" + +msgid "NotificationLevel|Custom" +msgstr "Персонализирани" + +msgid "NotificationLevel|Disabled" +msgstr "Изключени" + +msgid "NotificationLevel|Global" +msgstr "Глобални" + +msgid "NotificationLevel|On mention" +msgstr "При споменаване" + +msgid "NotificationLevel|Participate" +msgstr "Участие" + +msgid "NotificationLevel|Watch" +msgstr "Наблюдение" + +msgid "OfSearchInADropdown|Filter" +msgstr "Филтър" + msgid "OpenedNDaysAgo|Opened" msgstr "Отворен" +msgid "Options" +msgstr "Опции" + +msgid "Owner" +msgstr "Собственик" + +msgid "Pipeline" +msgstr "Схема" + msgid "Pipeline Health" msgstr "Състояние" +msgid "Pipeline Schedule" +msgstr "План за схема" + +msgid "Pipeline Schedules" +msgstr "Планове за схема" + +msgid "PipelineSchedules|Activated" +msgstr "Включено" + +msgid "PipelineSchedules|Active" +msgstr "Активно" + +msgid "PipelineSchedules|All" +msgstr "Всички" + +msgid "PipelineSchedules|Inactive" +msgstr "Неактивно" + +msgid "PipelineSchedules|Next Run" +msgstr "Следващо изпълнение" + +msgid "PipelineSchedules|None" +msgstr "Нищо" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "Въведете кратко описание за тази схема" + +msgid "PipelineSchedules|Take ownership" +msgstr "Поемане на собствеността" + +msgid "PipelineSchedules|Target" +msgstr "Цел" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "Проектът „%{project_name}“ е добавен в опашката за изтриване." + +msgid "Project '%{project_name}' was successfully created." +msgstr "Проектът „%{project_name}“ беше създаден успешно." + +msgid "Project '%{project_name}' was successfully updated." +msgstr "Проектът „%{project_name}“ беше обновен успешно." + +msgid "Project '%{project_name}' will be deleted." +msgstr "Проектът „%{project_name}“ ще бъде изтрит." + +msgid "Project access must be granted explicitly to each user." +msgstr "" +"Достъпът до проекта трябва да бъде даван поотделно на всеки потребител." + +msgid "Project export could not be deleted." +msgstr "Изнесените данни на проекта не могат да бъдат изтрити." + +msgid "Project export has been deleted." +msgstr "Изнесените данни на проекта бяха изтрити." + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "" +"Връзката към изнесените данни на проекта изгуби давност. Моля, създайте нова " +"от настройките на проекта." + +msgid "Project export started. A download link will be sent by email." +msgstr "" +"Изнасянето на проекта започна. Ще получите връзка към данните по е-поща." + +msgid "Project home" +msgstr "Начална страница на проекта" + +msgid "ProjectFeature|Disabled" +msgstr "Изключено" + +msgid "ProjectFeature|Everyone with access" +msgstr "Всеки с достъп" + +msgid "ProjectFeature|Only team members" +msgstr "Само членовете на екипа" + +msgid "ProjectFileTree|Name" +msgstr "Име" + +msgid "ProjectLastActivity|Never" +msgstr "Никога" + msgid "ProjectLifecycle|Stage" msgstr "Етап" +msgid "ProjectNetworkGraph|Graph" +msgstr "Графика" + msgid "Read more" msgstr "Прочетете повече" +msgid "Readme" +msgstr "ПрочетиМе" + +msgid "RefSwitcher|Branches" +msgstr "Клонове" + +msgid "RefSwitcher|Tags" +msgstr "Етикети" + msgid "Related Commits" msgstr "Свързани подавания" msgid "Related Deployed Jobs" -msgstr "Свързани задачи за внедряване" +msgstr "Свързани внедрени задачи" msgid "Related Issues" msgstr "Свързани проблеми" @@ -125,11 +665,87 @@ msgstr "Свързани заявки за сливане" msgid "Related Merged Requests" msgstr "Свързани приложени заявки за сливане" +msgid "Remind later" +msgstr "Напомняне по-късно" + +msgid "Remove project" +msgstr "Премахване на проекта" + +msgid "Request Access" +msgstr "Заявка за достъп" + +msgid "Revert this commit" +msgstr "Отмяна на това подаване" + +msgid "Revert this merge-request" +msgstr "Отмяна на тази заявка за сливане" + +msgid "Save pipeline schedule" +msgstr "Запазване на плана за схема" + +msgid "Schedule a new pipeline" +msgstr "Създаване на нов план за схема" + +msgid "Scheduling Pipelines" +msgstr "Планиране на схемите" + +msgid "Search branches and tags" +msgstr "Търсене в клоновете и етикетите" + +msgid "Select Archive Format" +msgstr "Изберете формата на архива" + +msgid "Select a timezone" +msgstr "Изберете часова зона" + +msgid "Select target branch" +msgstr "Изберете целеви клон" + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "" +"Задайте парола на профила си, за да можете да изтегляте и изпращате промени " +"чрез %{protocol}" + +msgid "Set up CI" +msgstr "Настройка на НИ" + +msgid "Set up Koding" +msgstr "Настройка на „Koding“" + +msgid "Set up auto deploy" +msgstr "Настройка на авт. внедряване" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "зададете парола" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Показване на %d събитие" msgstr[1] "Показване на %d събития" +msgid "Source code" +msgstr "Изходен код" + +msgid "StarProject|Star" +msgstr "Звезда" + +msgid "Start a <strong>new merge request</strong> with these changes" +msgstr "Създайте <strong>нова заявка за сливане</strong> с тези промени" + +msgid "Switch branch/tag" +msgstr "Преминаване към клон/етикет" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "Етикет" +msgstr[1] "Етикети" + +msgid "Tags" +msgstr "Етикети" + +msgid "Target Branch" +msgstr "Целеви клон" + msgid "" "The coding stage shows the time from the first commit to creating the merge " "request. The data will automatically be added here once you create your " @@ -142,6 +758,9 @@ msgstr "" msgid "The collection of events added to the data gathered for that stage." msgstr "Съвкупността от събития добавени към данните събрани за този етап." +msgid "The fork relationship has been removed." +msgstr "Връзката на разклонение беше премахната." + msgid "" "The issue stage shows the time it takes from creating an issue to assigning " "the issue to a milestone, or add the issue to a list on your Issue Board. " @@ -156,6 +775,15 @@ msgid "The phase of the development lifecycle." msgstr "Етапът от цикъла на разработка" msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "" +"Този план за схема ще изпълнява схемите в бъдеще, периодично, за определени " +"клонове или етикети. Тези планирани схеми ще наследят ограниченията на " +"достъпа до проекта на свързания с тях потребител." + +msgid "" "The planning stage shows the time from the previous step to pushing your " "first commit. This time will be added automatically once you push your first " "commit." @@ -170,7 +798,18 @@ msgid "" "once you have completed the full idea to production cycle." msgstr "" "Етапът на издаване показва общото време, което е нужно от създаването на " -"проблем до внедряването на кода в крайната версия." +"проблем до внедряването на кода в крайната версия. Данните ще бъдат добавени " +"автоматично след като завършите един пълен цикъл и превърнете първата си " +"идея в реалност." + +msgid "The project can be accessed by any logged in user." +msgstr "Всеки вписан потребител има достъп до проекта." + +msgid "The project can be accessed without any authentication." +msgstr "Всеки може да има достъп до проекта, без нужда от удостоверяване." + +msgid "The repository for this project does not exist." +msgstr "Хранилището за този проект не съществува." msgid "" "The review stage shows the time from creating the merge request to merging " @@ -197,8 +836,8 @@ msgid "" "first pipeline finishes running." msgstr "" "Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни " -"всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени " -"автоматично след като приключи изпълнените на първата Ви такава задача." +"всяка схема от задачи за свързаната заявка за сливане. Данните ще бъдат " +"добавени автоматично след като приключи изпълнението на първата Ви схема." msgid "The time taken by each data entry gathered by that stage." msgstr "Времето, което отнема всеки запис от данни за съответния етап." @@ -212,6 +851,13 @@ msgstr "" "данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е " "(5+7)/2 = 6." +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "" +"Това означава, че няма да можете да изпращате код, докато не създадете " +"празно хранилище или не внесете съществуващо такова." + msgid "Time before an issue gets scheduled" msgstr "Време преди един проблем да бъде планиран за работа" @@ -225,6 +871,129 @@ msgstr "" msgid "Time until first merge request" msgstr "Време преди първата заявка за сливане" +msgid "Timeago|%s days ago" +msgstr "преди %s дни" + +msgid "Timeago|%s days remaining" +msgstr "остават %s дни" + +msgid "Timeago|%s hours remaining" +msgstr "остават %s часа" + +msgid "Timeago|%s minutes ago" +msgstr "преди %s минути" + +msgid "Timeago|%s minutes remaining" +msgstr "остават %s минути" + +msgid "Timeago|%s months ago" +msgstr "преди %s месеца" + +msgid "Timeago|%s months remaining" +msgstr "остават %s месеца" + +msgid "Timeago|%s seconds remaining" +msgstr "остават %s секунди" + +msgid "Timeago|%s weeks ago" +msgstr "преди %s седмици" + +msgid "Timeago|%s weeks remaining" +msgstr "остават %s седмици" + +msgid "Timeago|%s years ago" +msgstr "преди %s години" + +msgid "Timeago|%s years remaining" +msgstr "остават %s години" + +msgid "Timeago|1 day remaining" +msgstr "остава 1 ден" + +msgid "Timeago|1 hour remaining" +msgstr "остава 1 час" + +msgid "Timeago|1 minute remaining" +msgstr "остава 1 минута" + +msgid "Timeago|1 month remaining" +msgstr "остава 1 месец" + +msgid "Timeago|1 week remaining" +msgstr "остава 1 седмица" + +msgid "Timeago|1 year remaining" +msgstr "остава 1 година" + +msgid "Timeago|Past due" +msgstr "Просрочено" + +msgid "Timeago|a day ago" +msgstr "преди един ден" + +msgid "Timeago|a month ago" +msgstr "преди един месец" + +msgid "Timeago|a week ago" +msgstr "преди една седмица" + +msgid "Timeago|a while" +msgstr "преди известно време" + +msgid "Timeago|a year ago" +msgstr "преди една година" + +msgid "Timeago|about %s hours ago" +msgstr "преди около %s часа" + +msgid "Timeago|about a minute ago" +msgstr "преди около една минута" + +msgid "Timeago|about an hour ago" +msgstr "преди около един час" + +msgid "Timeago|in %s days" +msgstr "след %s дни" + +msgid "Timeago|in %s hours" +msgstr "след %s часа" + +msgid "Timeago|in %s minutes" +msgstr "след %s минути" + +msgid "Timeago|in %s months" +msgstr "след %s месеца" + +msgid "Timeago|in %s seconds" +msgstr "след %s секунди" + +msgid "Timeago|in %s weeks" +msgstr "след %s седмици" + +msgid "Timeago|in %s years" +msgstr "след %s години" + +msgid "Timeago|in 1 day" +msgstr "след 1 ден" + +msgid "Timeago|in 1 hour" +msgstr "след 1 час" + +msgid "Timeago|in 1 minute" +msgstr "след 1 минута" + +msgid "Timeago|in 1 month" +msgstr "след 1 месец" + +msgid "Timeago|in 1 week" +msgstr "след 1 седмица" + +msgid "Timeago|in 1 year" +msgstr "след 1 година" + +msgid "Timeago|less than a minute ago" +msgstr "преди по-малко от минута" + msgid "Time|hr" msgid_plural "Time|hrs" msgstr[0] "час" @@ -244,17 +1013,121 @@ msgstr "Общо време" msgid "Total test time for all commits/merges" msgstr "Общо време за тестване на всички подавания/сливания" +msgid "Unstar" +msgstr "Без звезда" + +msgid "Upload New File" +msgstr "Качване на нов файл" + +msgid "Upload file" +msgstr "Качване на файл" + +msgid "Use your global notification setting" +msgstr "Използване на глобалната Ви настройка за известията" + +msgid "VisibilityLevel|Internal" +msgstr "Вътрешен" + +msgid "VisibilityLevel|Private" +msgstr "Частен" + +msgid "VisibilityLevel|Public" +msgstr "Публичен" + msgid "Want to see the data? Please ask an administrator for access." msgstr "Искате ли да видите данните? Помолете администратор за достъп." msgid "We don't have enough data to show this stage." msgstr "Няма достатъчно данни за този етап." +msgid "Withdraw Access Request" +msgstr "Оттегляне на заявката за достъп" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"На път сте да премахнете „%{project_name_with_namespace}“.\n" +"Ако го премахнете, той НЕ може да бъде възстановен!\n" +"НАИСТИНА ли искате това?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" +"На път сте да премахнете връзката на разклонението към оригиналния проект, " +"„%{forked_from_project}“. НАИСТИНА ли искате това?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "" +"На път сте да прехвърлите „%{project_name_with_namespace}“ към друг " +"собственик. НАИСТИНА ли искате това?" + +msgid "You can only add files when you are on a branch" +msgstr "Можете да добавяте файлове само когато се намирате в клон" + +msgid "You must sign in to star a project" +msgstr "Трябва да се впишете, за да отбележите проект със звезда" + msgid "You need permission." msgstr "Нуждаете се от разрешение." +msgid "You will not get any notifications via email" +msgstr "Няма да получавате никакви известия по е-поща" + +msgid "You will only receive notifications for the events you choose" +msgstr "Ще получавате известия само за събитията, за които желаете" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "Ще получавате известия само за нещата, в които участвате" + +msgid "You will receive notifications for any activity" +msgstr "Ще получавате известия за всяка дейност" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "Ще получавате известия само за коментари, в които Ви @споменават" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "" +"Няма да можете да изтегляте или изпращате код в проекта чрез %{protocol}, " +"докато не %{set_password_link} за профила си" + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "" +"Няма да можете да изтегляте или изпращате код в проекта чрез SSH, докато не " +"%{add_ssh_key_link} в профила си" + +msgid "Your name" +msgstr "Вашето име" + msgid "day" msgid_plural "days" msgstr[0] "ден" msgstr[1] "дни" +msgid "notification emails" +msgstr "известия по е-поща" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "родител" +msgstr[1] "родители" + +msgid "pipeline schedules documentation" +msgstr "документацията за планирането на схеми" + +msgid "with stage" +msgid_plural "with stages" +msgstr[0] "с етап" +msgstr[1] "с етапи" + diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po index 9a660571db9..ea864091b10 100644 --- a/locale/de/gitlab.po +++ b/locale/de/gitlab.po @@ -291,6 +291,9 @@ msgstr "Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Admin msgid "We don't have enough data to show this stage." msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen." +msgid "You have reached your project limit" +msgstr "" + msgid "You need permission." msgstr "Sie benötigen Zugriffsrechte." diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index 4e44731fc5a..afb8fb3176f 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -17,23 +17,217 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "\n" +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "" + +msgid "About auto deploy" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "Activity" +msgstr "" + +msgid "Add Changelog" +msgstr "" + +msgid "Add Contribution guide" +msgstr "" + +msgid "Add License" +msgstr "" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "" + +msgid "Add new directory" +msgstr "" + +msgid "Archived project! Repository is read-only" +msgstr "" + msgid "Are you sure you want to delete this pipeline schedule?" msgstr "" +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "" +msgstr[1] "" + +msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" +msgstr "" + +msgid "Branches" +msgstr "" + +msgid "Browse files" +msgstr "" + msgid "ByAuthor|by" msgstr "" +msgid "CI configuration" +msgstr "" + msgid "Cancel" msgstr "" +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "" + +msgid "ChangeTypeAction|Revert" +msgstr "" + +msgid "Changelog" +msgstr "" + +msgid "Charts" +msgstr "" + +msgid "Cherry-pick this commit" +msgstr "" + +msgid "Cherry-pick this merge request" +msgstr "" + +msgid "CiStatusLabel|canceled" +msgstr "" + +msgid "CiStatusLabel|created" +msgstr "" + +msgid "CiStatusLabel|failed" +msgstr "" + +msgid "CiStatusLabel|manual action" +msgstr "" + +msgid "CiStatusLabel|passed" +msgstr "" + +msgid "CiStatusLabel|passed with warnings" +msgstr "" + +msgid "CiStatusLabel|pending" +msgstr "" + +msgid "CiStatusLabel|skipped" +msgstr "" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "" + +msgid "CiStatusText|blocked" +msgstr "" + +msgid "CiStatusText|canceled" +msgstr "" + +msgid "CiStatusText|created" +msgstr "" + +msgid "CiStatusText|failed" +msgstr "" + +msgid "CiStatusText|manual" +msgstr "" + +msgid "CiStatusText|passed" +msgstr "" + +msgid "CiStatusText|pending" +msgstr "" + +msgid "CiStatusText|skipped" +msgstr "" + +msgid "CiStatus|running" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit message" +msgstr "" + +msgid "CommitBoxTitle|Commit" +msgstr "" + +msgid "CommitMessage|Add %{file_name}" +msgstr "" + +msgid "Commits" +msgstr "" + +msgid "Commits|History" +msgstr "" + +msgid "Committed by" +msgstr "" + +msgid "Compare" +msgstr "" + +msgid "Contribution guide" +msgstr "" + +msgid "Contributors" +msgstr "" + +msgid "Copy URL to clipboard" +msgstr "" + +msgid "Copy commit SHA to clipboard" +msgstr "" + +msgid "Create New Directory" +msgstr "" + +msgid "Create directory" +msgstr "" + +msgid "Create empty bare repository" +msgstr "" + +msgid "Create merge request" +msgstr "" + +msgid "Create new..." +msgstr "" + +msgid "CreateNewFork|Fork" +msgstr "" + +msgid "CreateTag|Tag" +msgstr "" + msgid "Cron Timezone" msgstr "" +msgid "Cron syntax" +msgstr "" + +msgid "Custom notification events" +msgstr "" + +msgid "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 %{notification_link}." +msgstr "" + +msgid "Cycle Analytics" +msgstr "" + msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" @@ -58,6 +252,9 @@ msgstr "" msgid "CycleAnalyticsStage|Test" msgstr "" +msgid "Define a custom pattern with cron syntax" +msgstr "" + msgid "Delete" msgstr "" @@ -69,19 +266,67 @@ msgstr[1] "" msgid "Description" msgstr "" +msgid "Directory name" +msgstr "" + +msgid "Don't show again" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Download tar" +msgstr "" + +msgid "Download tar.bz2" +msgstr "" + +msgid "Download tar.gz" +msgstr "" + +msgid "Download zip" +msgstr "" + +msgid "DownloadArtifacts|Download" +msgstr "" + +msgid "DownloadCommit|Email Patches" +msgstr "" + +msgid "DownloadCommit|Plain Diff" +msgstr "" + +msgid "DownloadSource|Download" +msgstr "" + msgid "Edit" msgstr "" msgid "Edit Pipeline Schedule %{id}" msgstr "" +msgid "Every day (at 4:00am)" +msgstr "" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "" + +msgid "Every week (Sundays at 4:00am)" +msgstr "" + msgid "Failed to change the owner" msgstr "" msgid "Failed to remove the pipeline schedule" msgstr "" -msgid "Filter" +msgid "Files" +msgstr "" + +msgid "Find by path" +msgstr "" + +msgid "Find file" msgstr "" msgid "FirstPushedBy|First" @@ -90,18 +335,47 @@ msgstr "" msgid "FirstPushedBy|pushed by" msgstr "" +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "" +msgstr[1] "" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "" + msgid "From issue creation until deploy to production" msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "Go to your fork" +msgstr "" + +msgid "GoToYourFork|Fork" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Housekeeping successfully started" +msgstr "" + +msgid "Import repository" +msgstr "" + msgid "Interval Pattern" msgstr "" msgid "Introducing Cycle Analytics" msgstr "" +msgid "LFSStatus|Disabled" +msgstr "" + +msgid "LFSStatus|Enabled" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "" @@ -110,6 +384,24 @@ msgstr[1] "" msgid "Last Pipeline" msgstr "" +msgid "Last Update" +msgstr "" + +msgid "Last commit" +msgstr "" + +msgid "Learn more in the" +msgstr "" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "" + +msgid "Leave group" +msgstr "" + +msgid "Leave project" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "" @@ -118,6 +410,9 @@ msgstr[1] "" msgid "Median" msgstr "" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "" @@ -126,6 +421,33 @@ msgstr[1] "" msgid "New Pipeline Schedule" msgstr "" +msgid "New branch" +msgstr "" + +msgid "New directory" +msgstr "" + +msgid "New file" +msgstr "" + +msgid "New issue" +msgstr "" + +msgid "New merge request" +msgstr "" + +msgid "New schedule" +msgstr "" + +msgid "New snippet" +msgstr "" + +msgid "New tag" +msgstr "" + +msgid "No repository" +msgstr "" + msgid "No schedules" msgstr "" @@ -135,12 +457,75 @@ msgstr "" msgid "Not enough data" msgstr "" +msgid "Notification events" +msgstr "" + +msgid "NotificationEvent|Close issue" +msgstr "" + +msgid "NotificationEvent|Close merge request" +msgstr "" + +msgid "NotificationEvent|Failed pipeline" +msgstr "" + +msgid "NotificationEvent|Merge merge request" +msgstr "" + +msgid "NotificationEvent|New issue" +msgstr "" + +msgid "NotificationEvent|New merge request" +msgstr "" + +msgid "NotificationEvent|New note" +msgstr "" + +msgid "NotificationEvent|Reassign issue" +msgstr "" + +msgid "NotificationEvent|Reassign merge request" +msgstr "" + +msgid "NotificationEvent|Reopen issue" +msgstr "" + +msgid "NotificationEvent|Successful pipeline" +msgstr "" + +msgid "NotificationLevel|Custom" +msgstr "" + +msgid "NotificationLevel|Disabled" +msgstr "" + +msgid "NotificationLevel|Global" +msgstr "" + +msgid "NotificationLevel|On mention" +msgstr "" + +msgid "NotificationLevel|Participate" +msgstr "" + +msgid "NotificationLevel|Watch" +msgstr "" + +msgid "OfSearchInADropdown|Filter" +msgstr "" + msgid "OpenedNDaysAgo|Opened" msgstr "" +msgid "Options" +msgstr "" + msgid "Owner" msgstr "" +msgid "Pipeline" +msgstr "" + msgid "Pipeline Health" msgstr "" @@ -177,12 +562,78 @@ msgstr "" msgid "PipelineSchedules|Target" msgstr "" +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "" + +msgid "Pipeline|with stage" +msgstr "" + +msgid "Pipeline|with stages" +msgstr "" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "" + +msgid "Project '%{project_name}' was successfully created." +msgstr "" + +msgid "Project '%{project_name}' was successfully updated." +msgstr "" + +msgid "Project '%{project_name}' will be deleted." +msgstr "" + +msgid "Project access must be granted explicitly to each user." +msgstr "" + +msgid "Project export could not be deleted." +msgstr "" + +msgid "Project export has been deleted." +msgstr "" + +msgid "Project export link has expired. Please generate a new export from your project settings." +msgstr "" + +msgid "Project export started. A download link will be sent by email." +msgstr "" + +msgid "Project home" +msgstr "" + +msgid "ProjectFeature|Disabled" +msgstr "" + +msgid "ProjectFeature|Everyone with access" +msgstr "" + +msgid "ProjectFeature|Only team members" +msgstr "" + +msgid "ProjectFileTree|Name" +msgstr "" + +msgid "ProjectLastActivity|Never" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "" +msgid "ProjectNetworkGraph|Graph" +msgstr "" + msgid "Read more" msgstr "" +msgid "Readme" +msgstr "" + +msgid "RefSwitcher|Branches" +msgstr "" + +msgid "RefSwitcher|Tags" +msgstr "" + msgid "Related Commits" msgstr "" @@ -201,23 +652,85 @@ msgstr "" msgid "Related Merged Requests" msgstr "" +msgid "Remind later" +msgstr "" + +msgid "Remove project" +msgstr "" + +msgid "Request Access" +msgstr "" + +msgid "Revert this commit" +msgstr "" + +msgid "Revert this merge request" +msgstr "" + msgid "Save pipeline schedule" msgstr "" msgid "Schedule a new pipeline" msgstr "" +msgid "Scheduling Pipelines" +msgstr "" + +msgid "Search branches and tags" +msgstr "" + +msgid "Select Archive Format" +msgstr "" + msgid "Select a timezone" msgstr "" msgid "Select target branch" msgstr "" +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "" + +msgid "Set up CI" +msgstr "" + +msgid "Set up Koding" +msgstr "" + +msgid "Set up auto deploy" +msgstr "" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "" msgstr[1] "" +msgid "Source code" +msgstr "" + +msgid "StarProject|Star" +msgstr "" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "" + +msgid "Start a <strong>new merge request</strong> with these changes" +msgstr "" + +msgid "Switch branch/tag" +msgstr "" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "" +msgstr[1] "" + +msgid "Tags" +msgstr "" + msgid "Target Branch" msgstr "" @@ -227,18 +740,33 @@ msgstr "" msgid "The collection of events added to the data gathered for that stage." msgstr "" +msgid "The fork relationship has been removed." +msgstr "" + msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "" msgid "The phase of the development lifecycle." msgstr "" +msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user." +msgstr "" + msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgstr "" msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgstr "" +msgid "The project can be accessed by any logged in user." +msgstr "" + +msgid "The project can be accessed without any authentication." +msgstr "" + +msgid "The repository for this project does not exist." +msgstr "" + msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "" @@ -254,6 +782,9 @@ msgstr "" msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." msgstr "" +msgid "This means you can not push code until you create an empty repository or import existing one." +msgstr "" + msgid "Time before an issue gets scheduled" msgstr "" @@ -266,6 +797,129 @@ msgstr "" msgid "Time until first merge request" msgstr "" +msgid "Timeago|%s days ago" +msgstr "" + +msgid "Timeago|%s days remaining" +msgstr "" + +msgid "Timeago|%s hours remaining" +msgstr "" + +msgid "Timeago|%s minutes ago" +msgstr "" + +msgid "Timeago|%s minutes remaining" +msgstr "" + +msgid "Timeago|%s months ago" +msgstr "" + +msgid "Timeago|%s months remaining" +msgstr "" + +msgid "Timeago|%s seconds remaining" +msgstr "" + +msgid "Timeago|%s weeks ago" +msgstr "" + +msgid "Timeago|%s weeks remaining" +msgstr "" + +msgid "Timeago|%s years ago" +msgstr "" + +msgid "Timeago|%s years remaining" +msgstr "" + +msgid "Timeago|1 day remaining" +msgstr "" + +msgid "Timeago|1 hour remaining" +msgstr "" + +msgid "Timeago|1 minute remaining" +msgstr "" + +msgid "Timeago|1 month remaining" +msgstr "" + +msgid "Timeago|1 week remaining" +msgstr "" + +msgid "Timeago|1 year remaining" +msgstr "" + +msgid "Timeago|Past due" +msgstr "" + +msgid "Timeago|a day ago" +msgstr "" + +msgid "Timeago|a month ago" +msgstr "" + +msgid "Timeago|a week ago" +msgstr "" + +msgid "Timeago|a while" +msgstr "" + +msgid "Timeago|a year ago" +msgstr "" + +msgid "Timeago|about %s hours ago" +msgstr "" + +msgid "Timeago|about a minute ago" +msgstr "" + +msgid "Timeago|about an hour ago" +msgstr "" + +msgid "Timeago|in %s days" +msgstr "" + +msgid "Timeago|in %s hours" +msgstr "" + +msgid "Timeago|in %s minutes" +msgstr "" + +msgid "Timeago|in %s months" +msgstr "" + +msgid "Timeago|in %s seconds" +msgstr "" + +msgid "Timeago|in %s weeks" +msgstr "" + +msgid "Timeago|in %s years" +msgstr "" + +msgid "Timeago|in 1 day" +msgstr "" + +msgid "Timeago|in 1 hour" +msgstr "" + +msgid "Timeago|in 1 minute" +msgstr "" + +msgid "Timeago|in 1 month" +msgstr "" + +msgid "Timeago|in 1 week" +msgstr "" + +msgid "Timeago|in 1 year" +msgstr "" + +msgid "Timeago|less than a minute ago" +msgstr "" + msgid "Time|hr" msgid_plural "Time|hrs" msgstr[0] "" @@ -285,16 +939,96 @@ msgstr "" msgid "Total test time for all commits/merges" msgstr "" +msgid "Unstar" +msgstr "" + +msgid "Upload New File" +msgstr "" + +msgid "Upload file" +msgstr "" + +msgid "Use your global notification setting" +msgstr "" + +msgid "VisibilityLevel|Internal" +msgstr "" + +msgid "VisibilityLevel|Private" +msgstr "" + +msgid "VisibilityLevel|Public" +msgstr "" + msgid "Want to see the data? Please ask an administrator for access." msgstr "" msgid "We don't have enough data to show this stage." msgstr "" +msgid "Withdraw Access Request" +msgstr "" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" + +msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" + +msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?" +msgstr "" + +msgid "You can only add files when you are on a branch" +msgstr "" + +msgid "You have reached your project limit" +msgstr "" + +msgid "You must sign in to star a project" +msgstr "" + msgid "You need permission." msgstr "" +msgid "You will not get any notifications via email" +msgstr "" + +msgid "You will only receive notifications for the events you choose" +msgstr "" + +msgid "You will only receive notifications for threads you have participated in" +msgstr "" + +msgid "You will receive notifications for any activity" +msgstr "" + +msgid "You will receive notifications only for comments in which you were @mentioned" +msgstr "" + +msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account" +msgstr "" + +msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile" +msgstr "" + +msgid "Your name" +msgstr "" + msgid "day" msgid_plural "days" msgstr[0] "" msgstr[1] "" + +msgid "new merge request" +msgstr "" + +msgid "notification emails" +msgstr "" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "" +msgstr[1] "" diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po new file mode 100644 index 00000000000..3ef9e19067d --- /dev/null +++ b/locale/eo/gitlab.po @@ -0,0 +1,1143 @@ +# Huang Tao <htve@outlook.com>, 2017. #zanata +# Lyubomir Vasilev <lyubomirv@abv.bg>, 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-20 06:24-0400\n" +"Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n" +"Language-Team: Esperanto (https://translate.zanata.org/project/view/GitLab)\n" +"Language: eo\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "%{commit_author_link} enmetis %{commit_timeago}" + +msgid "About auto deploy" +msgstr "Pri la aŭtomata disponigado" + +msgid "Active" +msgstr "Aktiva" + +msgid "Activity" +msgstr "Aktiveco" + +msgid "Add Changelog" +msgstr "Aldoni liston de ŝanĝoj" + +msgid "Add Contribution guide" +msgstr "Aldoni gvidliniojn por kontribuado" + +msgid "Add License" +msgstr "Aldoni rajtigilon" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "" +"Aldonu SSH-ŝlosilon al via profilo por ebligi al vi eltiri kaj alpuŝi per " +"SSH." + +msgid "Add new directory" +msgstr "Aldoni novan dosierujon" + +msgid "Archived project! Repository is read-only" +msgstr "Arkivita projekto! La deponejo permesas nur legadon" + +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "Ĉu vi certe volas forigi ĉi tiun ĉenstablan planon?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "Alkroĉu dosieron per ŝovmetado aŭ %{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "Branĉo" +msgstr[1] "Branĉoj" + +msgid "" +"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" +msgstr "" +"La branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aŭtomatan " +"disponigadon, bonvolu elekti Yaml-ŝablonon por GitLab CI kaj enmeti viajn " +"ŝanĝojn. %{link_to_autodeploy_doc}" + +msgid "Branches" +msgstr "Branĉoj" + +msgid "Browse files" +msgstr "Elekti dosierojn" + +msgid "ByAuthor|by" +msgstr "de" + +msgid "CI configuration" +msgstr "Agordoj de seninterrompa integrado" + +msgid "Cancel" +msgstr "Nuligi" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "Elekti en branĉon" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "Malfari en branĉo" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "Precize elekti" + +msgid "ChangeTypeAction|Revert" +msgstr "Malfari" + +msgid "Changelog" +msgstr "Listo de ŝanĝoj" + +msgid "Charts" +msgstr "Diagramoj" + +msgid "Cherry-pick this commit" +msgstr "Precize elekti ĉi tiun kunmetadon" + +msgid "Cherry-pick this merge request" +msgstr "Precize elekti ĉi tiun peton pri kunfando" + +msgid "CiStatusLabel|canceled" +msgstr "nuligita" + +msgid "CiStatusLabel|created" +msgstr "kreita" + +msgid "CiStatusLabel|failed" +msgstr "malsukcesa" + +msgid "CiStatusLabel|manual action" +msgstr "mana ago" + +msgid "CiStatusLabel|passed" +msgstr "sukcesa" + +msgid "CiStatusLabel|passed with warnings" +msgstr "sukcesa, kun avertoj" + +msgid "CiStatusLabel|pending" +msgstr "okazonta" + +msgid "CiStatusLabel|skipped" +msgstr "transsaltita" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "atendanta manan agon" + +msgid "CiStatusText|blocked" +msgstr "blokita" + +msgid "CiStatusText|canceled" +msgstr "nuligita" + +msgid "CiStatusText|created" +msgstr "kreita" + +msgid "CiStatusText|failed" +msgstr "malsukcesa" + +msgid "CiStatusText|manual" +msgstr "mana" + +msgid "CiStatusText|passed" +msgstr "sukcesa" + +msgid "CiStatusText|pending" +msgstr "okazonta" + +msgid "CiStatusText|skipped" +msgstr "transsaltita" + +msgid "CiStatus|running" +msgstr "plenumiĝanta" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Enmetado" +msgstr[1] "Enmetadoj" + +msgid "Commit message" +msgstr "Mesaĝo pri la enmetado" + +msgid "CommitBoxTitle|Commit" +msgstr "Enmeti" + +msgid "CommitMessage|Add %{file_name}" +msgstr "Aldoni „%{file_name}“" + +msgid "Commits" +msgstr "Enmetadoj" + +msgid "Commits|History" +msgstr "Historio" + +msgid "Committed by" +msgstr "Enmetita de" + +msgid "Compare" +msgstr "Kompari" + +msgid "Contribution guide" +msgstr "Gvidlinioj por kontribuado" + +msgid "Contributors" +msgstr "Kontribuantoj" + +msgid "Copy URL to clipboard" +msgstr "Kopii la adreson en la kopibufron" + +msgid "Copy commit SHA to clipboard" +msgstr "Kopii la identigilon de la enmetado" + +msgid "Create New Directory" +msgstr "Krei novan dosierujon" + +msgid "Create directory" +msgstr "Krei dosierujon" + +msgid "Create empty bare repository" +msgstr "Krei malplenan deponejon" + +msgid "Create merge request" +msgstr "Krei peton pri kunfando" + +msgid "Create new..." +msgstr "Krei novan…" + +msgid "CreateNewFork|Fork" +msgstr "Disbranĉigi" + +msgid "CreateTag|Tag" +msgstr "Etikedo" + +msgid "Cron Timezone" +msgstr "Horzono por Cron" + +msgid "Cron syntax" +msgstr "La sintakso de Cron" + +msgid "Custom notification events" +msgstr "Propraj sciigaj eventoj" + +msgid "" +"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 %{notification_link}." +msgstr "" +"La propraj sciigaj niveloj estas la samaj kiel la niveloj de partoprenado. " +"Uzante la proprajn sciigajn nivelojn, vi ricevos ankaŭ sciigojn por " +"elektitaj de vi eventoj. Por lerni pli, bonvolu vidi %{notification_link}." + +msgid "Cycle Analytics" +msgstr "Cikla analizo" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "" +"La cikla analizo esploras kiom da tempo necesas por disvolvi ideon ĝis ĝi " +"fariĝos realaĵo." + +msgid "CycleAnalyticsStage|Code" +msgstr "Programado" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Problemo" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Plano" + +msgid "CycleAnalyticsStage|Production" +msgstr "Eldonado" + +msgid "CycleAnalyticsStage|Review" +msgstr "Kontrolo" + +msgid "CycleAnalyticsStage|Staging" +msgstr "Preparo por eldono" + +msgid "CycleAnalyticsStage|Test" +msgstr "Testado" + +msgid "Define a custom pattern with cron syntax" +msgstr "Difini propran ŝablonon, uzante la sintakson de Cron" + +msgid "Delete" +msgstr "Forigi" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Disponigado" +msgstr[1] "Disponigadoj" + +msgid "Description" +msgstr "Priskribo" + +msgid "Directory name" +msgstr "Nomo de dosierujo" + +msgid "Don't show again" +msgstr "Ne montru denove" + +msgid "Download" +msgstr "Elŝuti" + +msgid "Download tar" +msgstr "Elŝuti en formato „tar“" + +msgid "Download tar.bz2" +msgstr "Elŝuti en formato „tar.bz2“" + +msgid "Download tar.gz" +msgstr "Elŝuti en formato „tar.gz“" + +msgid "Download zip" +msgstr "Elŝuti en formato „zip“" + +msgid "DownloadArtifacts|Download" +msgstr "Elŝuti" + +msgid "DownloadCommit|Email Patches" +msgstr "Sendi flikaĵojn per retpoŝto" + +msgid "DownloadCommit|Plain Diff" +msgstr "Normala dosiero kun diferencoj" + +msgid "DownloadSource|Download" +msgstr "Elŝuti" + +msgid "Edit" +msgstr "Redakti" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "Redakti ĉenstablan planon %{id}" + +msgid "Every day (at 4:00am)" +msgstr "Ĉiutage (je 4:00)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "Ĉiumonate (en la 1a de la monato, je 4:00)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "Ĉiusemajne (en dimanĉo, je 4:00)" + +msgid "Failed to change the owner" +msgstr "Ne eblas ŝanĝi la posedanton" + +msgid "Failed to remove the pipeline schedule" +msgstr "Ne eblas forigi la ĉenstablan planon" + +msgid "Files" +msgstr "Dosieroj" + +msgid "Find by path" +msgstr "Trovi per dosierindiko" + +msgid "Find file" +msgstr "Trovi dosieron" + +msgid "FirstPushedBy|First" +msgstr "Unue" + +msgid "FirstPushedBy|pushed by" +msgstr "alpuŝita de" + +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "Disbranĉigo" +msgstr[1] "Disbranĉigoj" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "Disbranĉigita el" + +msgid "From issue creation until deploy to production" +msgstr "De la kreado de la problemo ĝis la disponigado en la publika versio" + +msgid "From merge request merge until deploy to production" +msgstr "" +"De la kunfandado de la peto pri kunfando ĝis la disponigado en la publika " +"versio" + +msgid "Go to your fork" +msgstr "Al via disbranĉigo" + +msgid "GoToYourFork|Fork" +msgstr "Disbranĉigo" + +msgid "Home" +msgstr "Hejmo" + +msgid "Housekeeping successfully started" +msgstr "La refreŝigo komenciĝis sukcese" + +msgid "Import repository" +msgstr "Enporti deponejon" + +msgid "Interval Pattern" +msgstr "Intervala ŝablono" + +msgid "Introducing Cycle Analytics" +msgstr "Ni prezentas al vi la ciklan analizon" + +msgid "LFSStatus|Disabled" +msgstr "Malŝaltita" + +msgid "LFSStatus|Enabled" +msgstr "Ŝaltita" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "La lasta %d tago" +msgstr[1] "La lastaj %d tagoj" + +msgid "Last Pipeline" +msgstr "Lasta ĉenstablo" + +msgid "Last Update" +msgstr "Lasta ĝisdatigo" + +msgid "Last commit" +msgstr "Lasta enmetado" + +msgid "Learn more in the" +msgstr "Lernu pli en la" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "dokumentado pri ĉenstablaj planoj" + +msgid "Leave group" +msgstr "Forlasi la grupon" + +msgid "Leave project" +msgstr "Forlasi la projekton" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limigita al montrado de ne pli ol %d evento" +msgstr[1] "Limigita al montrado de ne pli ol %d eventoj" + +msgid "Median" +msgstr "Mediano" + +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "aldonos SSH-ŝlosilon" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nova problemo" +msgstr[1] "Novaj problemoj" + +msgid "New Pipeline Schedule" +msgstr "Nova ĉenstabla plano" + +msgid "New branch" +msgstr "Nova branĉo" + +msgid "New directory" +msgstr "Nova dosierujo" + +msgid "New file" +msgstr "Nova dosiero" + +msgid "New issue" +msgstr "Nova problemo" + +msgid "New merge request" +msgstr "Nova peto pri kunfando" + +msgid "New schedule" +msgstr "Nova plano" + +msgid "New snippet" +msgstr "Nova kodaĵo" + +msgid "New tag" +msgstr "Nova etikedo" + +msgid "No repository" +msgstr "Ne estas deponejo" + +msgid "No schedules" +msgstr "Ne estas planoj" + +msgid "Not available" +msgstr "Ne disponebla" + +msgid "Not enough data" +msgstr "Ne estas sufiĉe da datenoj" + +msgid "Notification events" +msgstr "Sciigaj eventoj" + +msgid "NotificationEvent|Close issue" +msgstr "Fermi problemon" + +msgid "NotificationEvent|Close merge request" +msgstr "Fermi peton pri kunfando" + +msgid "NotificationEvent|Failed pipeline" +msgstr "Malsukcesa ĉenstablo" + +msgid "NotificationEvent|Merge merge request" +msgstr "Apliki peton pri kunfando" + +msgid "NotificationEvent|New issue" +msgstr "Nova problemo" + +msgid "NotificationEvent|New merge request" +msgstr "Nova peto pri kunfando" + +msgid "NotificationEvent|New note" +msgstr "Nova noto" + +msgid "NotificationEvent|Reassign issue" +msgstr "Reatribui problemon" + +msgid "NotificationEvent|Reassign merge request" +msgstr "Reatribui peton pri kunfando" + +msgid "NotificationEvent|Reopen issue" +msgstr "Remalfermi problemon" + +msgid "NotificationEvent|Successful pipeline" +msgstr "Sukcesa ĉenstablo" + +msgid "NotificationLevel|Custom" +msgstr "Propraj" + +msgid "NotificationLevel|Disabled" +msgstr "Malŝaltitaj" + +msgid "NotificationLevel|Global" +msgstr "Ĝeneralaj" + +msgid "NotificationLevel|On mention" +msgstr "Ĉe mencio" + +msgid "NotificationLevel|Participate" +msgstr "Partoprenado" + +msgid "NotificationLevel|Watch" +msgstr "Rigardado" + +msgid "OfSearchInADropdown|Filter" +msgstr "Filtrilo" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Malfermita" + +msgid "Options" +msgstr "Opcioj" + +msgid "Owner" +msgstr "Posedanto" + +msgid "Pipeline" +msgstr "Ĉenstablo" + +msgid "Pipeline Health" +msgstr "Stato" + +msgid "Pipeline Schedule" +msgstr "Ĉenstabla plano" + +msgid "Pipeline Schedules" +msgstr "Ĉenstablaj planoj" + +msgid "PipelineSchedules|Activated" +msgstr "Ŝaltita" + +msgid "PipelineSchedules|Active" +msgstr "Ŝaltitaj" + +msgid "PipelineSchedules|All" +msgstr "Ĉiuj" + +msgid "PipelineSchedules|Inactive" +msgstr "Malŝaltitaj" + +msgid "PipelineSchedules|Next Run" +msgstr "Sekvanta plenumo" + +msgid "PipelineSchedules|None" +msgstr "Nenio" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "Entajpu mallongan priskribon pri ĉi tiu ĉenstablo" + +msgid "PipelineSchedules|Take ownership" +msgstr "Akiri posedon" + +msgid "PipelineSchedules|Target" +msgstr "Celo" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "Propra" + +msgid "Pipeline|with stage" +msgstr "kun etapo" + +msgid "Pipeline|with stages" +msgstr "kun etapoj" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "La projekto „%{project_name}“ estis alvicigita por forigado." + +msgid "Project '%{project_name}' was successfully created." +msgstr "La projekto „%{project_name}“ estis sukcese kreita." + +msgid "Project '%{project_name}' was successfully updated." +msgstr "La projekto „%{project_name}“ estis sukcese ĝisdatigita." + +msgid "Project '%{project_name}' will be deleted." +msgstr "La projekto „%{project_name}“ estos forigita." + +msgid "Project access must be granted explicitly to each user." +msgstr "Ĉiu uzanto devas akiri propran atingon al la projekto." + +msgid "Project export could not be deleted." +msgstr "Ne eblas forigi la projektan elporton." + +msgid "Project export has been deleted." +msgstr "La projekta elporto estis forigita." + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "" +"La ligilo por la projekta elporto eksvalidiĝis. Bonvolu krei novan elporton " +"en la agordoj de la projekto." + +msgid "Project export started. A download link will be sent by email." +msgstr "" +"La elporto de la projekto komenciĝis. Vi ricevos ligilon per retpoŝto por " +"elŝuti la datenoj." + +msgid "Project home" +msgstr "Hejmo de la projekto" + +msgid "ProjectFeature|Disabled" +msgstr "Malŝaltita" + +msgid "ProjectFeature|Everyone with access" +msgstr "Ĉiu, kiu havas atingon" + +msgid "ProjectFeature|Only team members" +msgstr "Nur skipanoj" + +msgid "ProjectFileTree|Name" +msgstr "Nomo" + +msgid "ProjectLastActivity|Never" +msgstr "Neniam" + +msgid "ProjectLifecycle|Stage" +msgstr "Etapo" + +msgid "ProjectNetworkGraph|Graph" +msgstr "Grafeo" + +msgid "Read more" +msgstr "Legu pli" + +msgid "Readme" +msgstr "LeguMin" + +msgid "RefSwitcher|Branches" +msgstr "Branĉoj" + +msgid "RefSwitcher|Tags" +msgstr "Etikedoj" + +msgid "Related Commits" +msgstr "Rilataj enmetadoj" + +msgid "Related Deployed Jobs" +msgstr "Rilataj disponigitaj taskoj" + +msgid "Related Issues" +msgstr "Rilataj problemoj" + +msgid "Related Jobs" +msgstr "Rilataj taskoj" + +msgid "Related Merge Requests" +msgstr "Rilataj petoj pri kunfando" + +msgid "Related Merged Requests" +msgstr "Rilataj aplikitaj petoj pri kunfando" + +msgid "Remind later" +msgstr "Rememorigu denove" + +msgid "Remove project" +msgstr "Forigi la projekton" + +msgid "Request Access" +msgstr "Peti atingeblon" + +msgid "Revert this commit" +msgstr "Malfari ĉi tiun enmetadon" + +msgid "Revert this merge request" +msgstr "Malfari ĉi tiun peton pri kunfando" + +msgid "Save pipeline schedule" +msgstr "Konservi ĉenstablan planon" + +msgid "Schedule a new pipeline" +msgstr "Plani novan ĉenstablon" + +msgid "Scheduling Pipelines" +msgstr "Planado de la ĉenstabloj" + +msgid "Search branches and tags" +msgstr "Serĉu branĉon aŭ etikedon" + +msgid "Select Archive Format" +msgstr "Elektu formaton de arkivo" + +msgid "Select a timezone" +msgstr "Elektu horzonon" + +msgid "Select target branch" +msgstr "Elektu celan branĉon" + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "" +"Kreu pasvorton por via konto por ebligi al vi eltiri kaj alpuŝi per " +"%{protocol}" + +msgid "Set up CI" +msgstr "Agordi SI" + +msgid "Set up Koding" +msgstr "Agordi „Koding“" + +msgid "Set up auto deploy" +msgstr "Agordi aŭtomatan disponigadon" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "kreos pasvorton" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Estas montrata %d evento" +msgstr[1] "Estas montrataj %d eventoj" + +msgid "Source code" +msgstr "Kodo" + +msgid "StarProject|Star" +msgstr "Steligi" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "Kreu %{new_merge_request} kun ĉi tiuj ŝanĝoj" + +msgid "Switch branch/tag" +msgstr "Iri al branĉo/etikedo" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "Etikedo" +msgstr[1] "Etikedoj" + +msgid "Tags" +msgstr "Etikedoj" + +msgid "Target Branch" +msgstr "Cela branĉo" + +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"La etapo de programado montras la tempon de la unua enmetado ĝis la kreado " +"de la peto pri kunfando. La datenoj aldoniĝos aŭtomate ĉi tie post kiam vi " +"kreas la unuan peton pri kunfando." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "" +"La aro da eventoj, kiuj estas aldonitaj al la datenoj kolektitaj por la " +"etapo." + +msgid "The fork relationship has been removed." +msgstr "La rilato de disbranĉigo estis forigita." + +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "" +"La etapo de la problemo montras kiom la tempo pasas de la kreado de problemo " +"ĝis la atribuado de la problemo al cela etapo de la projekto, aŭ al listo " +"sur la problemtabulo. Komencu krei problemojn por vidi la datenojn por ĉi " +"tiu etapo." + +msgid "The phase of the development lifecycle." +msgstr "La etapo de la disvolva ciklo." + +msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "" +"La ĉenstabla plano plenumas ĉenstablojn en la estonteco, ripete, por " +"difinitaj branĉoj aŭ etikedoj. Tiuj planitaj ĉenstabloj heredos la limigitan " +"atingon al la projekto de la rilata uzanto." + +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "" +"La etapo de la plano montras la tempon de la antaŭa ŝtupo ĝis la alpuŝado de " +"via unua enmetado. Ĉi tiu tempo aldoniĝos aŭtomate post kiam vi alpuŝas la " +"unuan enmetadon." + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "" +"La etapo de eldonado montras la tutan tempon de la kreado de problemo ĝis la " +"disponigado en la publika versio. La datenoj aldoniĝos aŭtomate post kiam vi " +"kompletigos plenan ciklon de ideo ĝis realaĵo." + +msgid "The project can be accessed by any logged in user." +msgstr "Ĉiu ensalutita uzanto havas atingon al la projekto" + +msgid "The project can be accessed without any authentication." +msgstr "Ĉiu povas havi atingon al la projekto, sen ensaluti" + +msgid "The repository for this project does not exist." +msgstr "La deponejo por ĉi tiu projekto ne ekzistas." + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"La etapo de la kontrolo montras la tempon de la kreado de la peto pri " +"kunfando ĝis ĝia aplikado. La datenoj aldoniĝos aŭtomate post kiam vi " +"aplikos la unuan peton pri kunfando." + +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "" +"La etapo de preparo por eldono montras la tempon inter la aplikado de la " +"peto pri kunfando kaj la disponigado de la kodo en la publika versio. La " +"datenoj aldoniĝos aŭtomate post kiam vi faros la unuan disponigadon en la " +"publika versio." + +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "" +"La etapo de testado montras kiom da tempo necesas al „GitLab CI“ por plenumi " +"ĉiujn ĉenstablojn por la rilata peto pri kunfando. La datenoj aldoniĝos " +"aŭtomate post kiam via unua ĉenstablo finiĝos." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "La tempo, kiu estas necesa por ĉiu dateno kolektita de la etapo." + +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." +msgstr "" +"La valoro, kiu troviĝas en la mezo de aro da rigardataj valoroj. Ekzemple: " +"inter 3, 5 kaj 9, la mediano estas 5. Inter 3, 5, 7 kaj 8, la mediano estas " +"(5+7)/2 = 6." + +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "" +"Ĉi tiu signifas, ke vi ne povos alpuŝi kodon, antaŭ ol vi kreos malplenan " +"deponejon aŭ enportos jam ekzistantan." + +msgid "Time before an issue gets scheduled" +msgstr "Tempo antaŭ problemo estas planita por ellabori" + +msgid "Time before an issue starts implementation" +msgstr "Tempo antaŭ la komenco de laboro super problemo" + +msgid "Time between merge request creation and merge/close" +msgstr "Tempo inter la kreado de poeto pri kunfando kaj ĝia aplikado/fermado" + +msgid "Time until first merge request" +msgstr "Tempo ĝis la unua peto pri kunfando" + +msgid "Timeago|%s days ago" +msgstr "antaŭ %s tagoj" + +msgid "Timeago|%s days remaining" +msgstr "restas %s tagoj" + +msgid "Timeago|%s hours remaining" +msgstr "restas %s horoj" + +msgid "Timeago|%s minutes ago" +msgstr "antaŭ %s minutoj" + +msgid "Timeago|%s minutes remaining" +msgstr "restas %s minutoj" + +msgid "Timeago|%s months ago" +msgstr "antaŭ %s monatoj" + +msgid "Timeago|%s months remaining" +msgstr "restas %s monatoj" + +msgid "Timeago|%s seconds remaining" +msgstr "restas %s sekundoj" + +msgid "Timeago|%s weeks ago" +msgstr "antaŭ %s semajnoj" + +msgid "Timeago|%s weeks remaining" +msgstr "restas %s semajnoj" + +msgid "Timeago|%s years ago" +msgstr "antaŭ %s jaroj" + +msgid "Timeago|%s years remaining" +msgstr "restas %s jaroj" + +msgid "Timeago|1 day remaining" +msgstr "restas 1 tago" + +msgid "Timeago|1 hour remaining" +msgstr "restas 1 horo" + +msgid "Timeago|1 minute remaining" +msgstr "restas 1 minuto" + +msgid "Timeago|1 month remaining" +msgstr "restas 1 monato" + +msgid "Timeago|1 week remaining" +msgstr "restas 1 semajno" + +msgid "Timeago|1 year remaining" +msgstr "restas 1 jaro" + +msgid "Timeago|Past due" +msgstr "Malfruiĝis" + +msgid "Timeago|a day ago" +msgstr "antaŭ unu tago" + +msgid "Timeago|a month ago" +msgstr "antaŭ unu monato" + +msgid "Timeago|a week ago" +msgstr "antaŭ unu semajno" + +msgid "Timeago|a while" +msgstr "antaŭ iom da tempo" + +msgid "Timeago|a year ago" +msgstr "antaŭ unu jaro" + +msgid "Timeago|about %s hours ago" +msgstr "antaŭ ĉirkaŭ %s horoj" + +msgid "Timeago|about a minute ago" +msgstr "antaŭ ĉirkaŭ unu minuto" + +msgid "Timeago|about an hour ago" +msgstr "antaŭ ĉirkaŭ unu horo" + +msgid "Timeago|in %s days" +msgstr "post %s tagoj" + +msgid "Timeago|in %s hours" +msgstr "post %s horoj" + +msgid "Timeago|in %s minutes" +msgstr "post %s minutoj" + +msgid "Timeago|in %s months" +msgstr "post %s monatoj" + +msgid "Timeago|in %s seconds" +msgstr "post %s sekundoj" + +msgid "Timeago|in %s weeks" +msgstr "post %s semajnoj" + +msgid "Timeago|in %s years" +msgstr "post %s jaroj" + +msgid "Timeago|in 1 day" +msgstr "post 1 tago" + +msgid "Timeago|in 1 hour" +msgstr "post 1 horo" + +msgid "Timeago|in 1 minute" +msgstr "post 1 minuto" + +msgid "Timeago|in 1 month" +msgstr "post 1 monato" + +msgid "Timeago|in 1 week" +msgstr "post 1 semajno" + +msgid "Timeago|in 1 year" +msgstr "post 1 jaro" + +msgid "Timeago|less than a minute ago" +msgstr "antaŭ malpli ol minuto" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "h" +msgstr[1] "h" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "min" +msgstr[1] "min" + +msgid "Time|s" +msgstr "s" + +msgid "Total Time" +msgstr "Totala tempo" + +msgid "Total test time for all commits/merges" +msgstr "Totala tempo por la testado de ĉiuj enmetadoj/kunfandoj" + +msgid "Unstar" +msgstr "Malsteligi" + +msgid "Upload New File" +msgstr "Alŝuti novan dosieron" + +msgid "Upload file" +msgstr "Alŝuti dosieron" + +msgid "Use your global notification setting" +msgstr "Uzi vian ĝeneralan agordon pri la sciigoj" + +msgid "VisibilityLevel|Internal" +msgstr "Interna" + +msgid "VisibilityLevel|Private" +msgstr "Privata" + +msgid "VisibilityLevel|Public" +msgstr "Publika" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "" +"Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto." + +msgid "We don't have enough data to show this stage." +msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon." + +msgid "Withdraw Access Request" +msgstr "Nuligi la peton pri atingeblo" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"Vi forigos „%{project_name_with_namespace}“.\n" +"Oni NE POVAS malfari la forigon de projekto!\n" +"Ĉu vi estas ABSOLUTE certa?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" +"Vi forigos la rilaton de la disbranĉigo al la originala projekto, " +"„%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "" +"Vi transigos „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas " +"ABSOLUTE certa?" + +msgid "You can only add files when you are on a branch" +msgstr "Oni povas aldoni dosierojn nur kiam oni estas en branĉo" + +msgid "You have reached your project limit" +msgstr "Vi ne povas krei pliajn projektojn" + +msgid "You must sign in to star a project" +msgstr "Oni devas ensaluti por steligi projekton" + +msgid "You need permission." +msgstr "VI bezonas permeson." + +msgid "You will not get any notifications via email" +msgstr "VI ne ricevos sciigojn per retpoŝto" + +msgid "You will only receive notifications for the events you choose" +msgstr "Vi ricevos sciigojn nur por la eventoj elektitaj de vi" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "Vi ricevos sciigojn nur por la fadenoj, en kiuj vi partoprenis" + +msgid "You will receive notifications for any activity" +msgstr "Vi ricevos sciigojn por ĉiu ago" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "Vi ricevos sciigojn nur por komentoj, en kiuj vi estas @menciita" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "" +"Vi ne povos eltiri aŭ alpuŝi kodon per %{protocol} antaŭ ol vi " +"%{set_password_link} por via konto" + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "" +"Vi ne povos eltiri aŭ alpuŝi kodon per SSH antaŭ ol vi %{add_ssh_key_link} " +"al via profilo" + +msgid "Your name" +msgstr "Via nomo" + +msgid "day" +msgid_plural "days" +msgstr[0] "tago" +msgstr[1] "tagoj" + +msgid "new merge request" +msgstr "novan peton pri kunfando" + +msgid "notification emails" +msgstr "sciigoj per retpoŝto" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "patro" +msgstr[1] "patroj" + diff --git a/locale/eo/gitlab.po.time_stamp b/locale/eo/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/eo/gitlab.po.time_stamp diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index 78d28d69885..cc44a06cbc5 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-06-07 12:29-0500\n" +"PO-Revision-Date: 2017-06-21 12:09-0500\n" "Language-Team: Spanish\n" "Language: es\n" "MIME-Version: 1.0\n" @@ -17,9 +17,25 @@ msgstr "" "Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n" "X-Generator: Poedit 2.0.2\n" +msgid "%d additional commit has been omitted to prevent performance issues." +msgid_plural "%d additional commits have been omitted to prevent performance issues." +msgstr[0] "%d cambio adicional ha sido omitido para evitar problemas de rendimiento." +msgstr[1] "%d cambios adicionales han sido omitidos para evitar problemas de rendimiento." + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "%d cambio" +msgstr[1] "%d cambios" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "%{commit_author_link} cambió %{commit_timeago}" + msgid "About auto deploy" msgstr "Acerca del auto despliegue" +msgid "Active" +msgstr "Activo" + msgid "Activity" msgstr "Actividad" @@ -39,7 +55,13 @@ msgid "Add new directory" msgstr "Agregar nuevo directorio" msgid "Archived project! Repository is read-only" -msgstr "¡Proyecto archivado! El repositorio es de sólo lectura" +msgstr "¡Proyecto archivado! El repositorio es de solo lectura" + +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "¿Estás seguro que deseas eliminar esta programación del pipeline?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "Adjunte un archivo arrastrando & soltando o %{upload_link}" msgid "Branch" msgid_plural "Branches" @@ -49,21 +71,60 @@ msgstr[1] "Ramas" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}" +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "Buscar ramas" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "Cambiar rama" + msgid "Branches" msgstr "Ramas" +msgid "Browse Directory" +msgstr "Examinar directorio" + +msgid "Browse File" +msgstr "Examinar archivo" + +msgid "Browse Files" +msgstr "Examinar archivos" + +msgid "Browse files" +msgstr "Examinar archivos" + msgid "ByAuthor|by" msgstr "por" msgid "CI configuration" msgstr "Configuración de CI" +msgid "Cancel" +msgstr "Cancelar" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "Escoger en la rama" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "Revertir en la rama" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "Cherry-pick" + +msgid "ChangeTypeAction|Revert" +msgstr "Revertir" + msgid "Changelog" msgstr "Changelog" msgid "Charts" msgstr "Gráficos" +msgid "Cherry-pick this commit" +msgstr "Escoger este cambio" + +msgid "Cherry-pick this merge request" +msgstr "Escoger esta solicitud de fusión" + msgid "CiStatusLabel|canceled" msgstr "cancelado" @@ -71,7 +132,7 @@ msgid "CiStatusLabel|created" msgstr "creado" msgid "CiStatusLabel|failed" -msgstr "fallado" +msgstr "fallido" msgid "CiStatusLabel|manual action" msgstr "acción manual" @@ -123,15 +184,27 @@ msgid_plural "Commits" msgstr[0] "Cambio" msgstr[1] "Cambios" +msgid "Commit message" +msgstr "Mensaje del cambio" + +msgid "CommitBoxTitle|Commit" +msgstr "Cambio" + msgid "CommitMessage|Add %{file_name}" msgstr "Agregar %{file_name}" msgid "Commits" msgstr "Cambios" +msgid "Commits feed" +msgstr "Feed de cambios" + msgid "Commits|History" msgstr "Historial" +msgid "Committed by" +msgstr "Enviado por" + msgid "Compare" msgstr "Comparar" @@ -159,9 +232,21 @@ msgstr "Crear repositorio vacío" msgid "Create merge request" msgstr "Crear solicitud de fusión" +msgid "Create new..." +msgstr "Crear nuevo..." + msgid "CreateNewFork|Fork" msgstr "Bifurcar" +msgid "CreateTag|Tag" +msgstr "Etiqueta" + +msgid "Cron Timezone" +msgstr "Zona horaria del Cron" + +msgid "Cron syntax" +msgstr "Sintaxis de Cron" + msgid "Custom notification events" msgstr "Eventos de notificaciones personalizadas" @@ -195,17 +280,29 @@ msgstr "Puesta en escena" msgid "CycleAnalyticsStage|Test" msgstr "Pruebas" +msgid "Define a custom pattern with cron syntax" +msgstr "Definir un patrón personalizado con la sintaxis de cron" + +msgid "Delete" +msgstr "Eliminar" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "Despliegue" msgstr[1] "Despliegues" +msgid "Description" +msgstr "Descripción" + msgid "Directory name" msgstr "Nombre del directorio" msgid "Don't show again" msgstr "No mostrar de nuevo" +msgid "Download" +msgstr "Descargar" + msgid "Download tar" msgstr "Descargar tar" @@ -221,12 +318,42 @@ msgstr "Descargar zip" msgid "DownloadArtifacts|Download" msgstr "Descargar" +msgid "DownloadCommit|Email Patches" +msgstr "Parches por correo electrónico" + +msgid "DownloadCommit|Plain Diff" +msgstr "Diferencias en texto plano" + msgid "DownloadSource|Download" msgstr "Descargar" +msgid "Edit" +msgstr "Editar" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "Editar Programación del Pipeline %{id}" + +msgid "Every day (at 4:00am)" +msgstr "Todos los días (a las 4:00 am)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "Todos los meses (el día 1 a las 4:00 am)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "Todas las semanas (domingos a las 4:00 am)" + +msgid "Failed to change the owner" +msgstr "Error al cambiar el propietario" + +msgid "Failed to remove the pipeline schedule" +msgstr "Error al eliminar la programación del pipeline" + msgid "Files" msgstr "Archivos" +msgid "Filter by commit message" +msgstr "Filtrar por mensaje del cambio" + msgid "Find by path" msgstr "Buscar por ruta" @@ -239,12 +366,14 @@ msgstr "Primer" msgid "FirstPushedBy|pushed by" msgstr "enviado por" +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "Bifurcación" +msgstr[1] "Bifurcaciones" + msgid "ForkedFromProjectPath|Forked from" msgstr "Bifurcado de" -msgid "Forks" -msgstr "Bifurcaciones" - msgid "From issue creation until deploy to production" msgstr "Desde la creación de la incidencia hasta el despliegue a producción" @@ -266,6 +395,9 @@ msgstr "Servicio de limpieza iniciado con éxito" msgid "Import repository" msgstr "Importar repositorio" +msgid "Interval Pattern" +msgstr "Patrón de intervalo" + msgid "Introducing Cycle Analytics" msgstr "Introducción a Cycle Analytics" @@ -280,12 +412,21 @@ msgid_plural "Last %d days" msgstr[0] "Último %d día" msgstr[1] "Últimos %d días" +msgid "Last Pipeline" +msgstr "Último Pipeline" + msgid "Last Update" msgstr "Última actualización" msgid "Last commit" msgstr "Último cambio" +msgid "Learn more in the" +msgstr "Más información en la" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "documentación sobre la programación de pipelines" + msgid "Leave group" msgstr "Abandonar grupo" @@ -308,6 +449,9 @@ msgid_plural "New Issues" msgstr[0] "Nueva incidencia" msgstr[1] "Nuevas incidencias" +msgid "New Pipeline Schedule" +msgstr "Nueva Programación del Pipeline" + msgid "New branch" msgstr "Nueva rama" @@ -323,6 +467,9 @@ msgstr "Nueva incidencia" msgid "New merge request" msgstr "Nueva solicitud de fusión" +msgid "New schedule" +msgstr "Nueva programación" + msgid "New snippet" msgstr "Nuevo fragmento de código" @@ -332,6 +479,9 @@ msgstr "Nueva etiqueta" msgid "No repository" msgstr "No hay repositorio" +msgid "No schedules" +msgstr "No hay programaciones" + msgid "Not available" msgstr "No disponible" @@ -392,12 +542,66 @@ msgstr "Participación" msgid "NotificationLevel|Watch" msgstr "Vigilancia" +msgid "OfSearchInADropdown|Filter" +msgstr "Filtrar" + msgid "OpenedNDaysAgo|Opened" msgstr "Abierto" +msgid "Options" +msgstr "Opciones" + +msgid "Owner" +msgstr "Propietario" + +msgid "Pipeline" +msgstr "Pipeline" + msgid "Pipeline Health" msgstr "Estado del Pipeline" +msgid "Pipeline Schedule" +msgstr "Programación del Pipeline" + +msgid "Pipeline Schedules" +msgstr "Programaciones de los Pipelines" + +msgid "PipelineSchedules|Activated" +msgstr "Activado" + +msgid "PipelineSchedules|Active" +msgstr "Activos" + +msgid "PipelineSchedules|All" +msgstr "Todos" + +msgid "PipelineSchedules|Inactive" +msgstr "Inactivos" + +msgid "PipelineSchedules|Next Run" +msgstr "Próxima Ejecución" + +msgid "PipelineSchedules|None" +msgstr "Ninguno" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "Proporcione una breve descripción para este pipeline" + +msgid "PipelineSchedules|Take ownership" +msgstr "Tomar posesión" + +msgid "PipelineSchedules|Target" +msgstr "Destino" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "Personalizado" + +msgid "Pipeline|with stage" +msgstr "con etapa" + +msgid "Pipeline|with stages" +msgstr "con etapas" + msgid "Project '%{project_name}' queued for deletion." msgstr "Proyecto ‘%{project_name}’ en cola para eliminación." @@ -453,7 +657,7 @@ msgid "Read more" msgstr "Leer más" msgid "Readme" -msgstr "Readme" +msgstr "Léeme" msgid "RefSwitcher|Branches" msgstr "Ramas" @@ -488,14 +692,35 @@ msgstr "Eliminar proyecto" msgid "Request Access" msgstr "Solicitar acceso" +msgid "Revert this commit" +msgstr "Revertir este cambio" + +msgid "Revert this merge request" +msgstr "Revertir esta solicitud de fusión" + +msgid "Save pipeline schedule" +msgstr "Guardar programación del pipeline" + +msgid "Schedule a new pipeline" +msgstr "Programar un nuevo pipeline" + +msgid "Scheduling Pipelines" +msgstr "Programación de Pipelines" + msgid "Search branches and tags" msgstr "Buscar ramas y etiquetas" msgid "Select Archive Format" msgstr "Seleccionar formato de archivo" +msgid "Select a timezone" +msgstr "Selecciona una zona horaria" + +msgid "Select target branch" +msgstr "Selecciona una rama de destino" + msgid "Set a password on your account to pull or push via %{protocol}" -msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de% {protocol}" +msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}" msgid "Set up CI" msgstr "Configurar CI" @@ -520,6 +745,9 @@ msgstr "Código fuente" msgid "StarProject|Star" msgstr "Destacar" +msgid "Start a %{new_merge_request} with these changes" +msgstr "Iniciar una %{new_merge_request} con estos cambios" + msgid "Switch branch/tag" msgstr "Cambiar rama/etiqueta" @@ -531,6 +759,9 @@ msgstr[1] "Etiquetas" msgid "Tags" msgstr "Etiquetas" +msgid "Target Branch" +msgstr "Rama de destino" + msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión." @@ -546,6 +777,9 @@ msgstr "La etapa de incidencia muestra el tiempo que toma desde la creación de msgid "The phase of the development lifecycle." msgstr "La etapa del ciclo de vida de desarrollo." +msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user." +msgstr "La programación de pipelines ejecuta pipelines en el futuro, repetidamente, para ramas o etiquetas específicas. Los pipelines programados heredarán acceso limitado al proyecto basado en su usuario asociado." + msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgstr "La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio." @@ -652,16 +886,16 @@ msgid "Timeago|a day ago" msgstr "hace un día" msgid "Timeago|a month ago" -msgstr "hace 1 mes" +msgstr "hace un mes" msgid "Timeago|a week ago" -msgstr "hace 1 semana" +msgstr "hace una semana" msgid "Timeago|a while" msgstr "hace un momento" msgid "Timeago|a year ago" -msgstr "hace 1 año" +msgstr "hace un año" msgid "Timeago|about %s hours ago" msgstr "hace alrededor de %s horas" @@ -742,9 +976,15 @@ msgstr "Subir nuevo archivo" msgid "Upload file" msgstr "Subir archivo" +msgid "UploadLink|click to upload" +msgstr "Hacer clic para subir" + msgid "Use your global notification setting" msgstr "Utiliza tu configuración de notificación global" +msgid "View open merge request" +msgstr "Ver solicitud de fusión abierta" + msgid "VisibilityLevel|Internal" msgstr "Interno" @@ -772,14 +1012,17 @@ msgstr "" "¡El proyecto eliminado NO puede ser restaurado!\n" "¿Estás TOTALMENTE seguro?" -msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" +msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?" msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?" msgstr "Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?" msgid "You can only add files when you are on a branch" -msgstr "Sólo puede agregar archivos cuando estas en una rama" +msgstr "Solo puedes agregar archivos cuando estás en una rama" + +msgid "You have reached your project limit" +msgstr "Has alcanzado el límite de tu proyecto" msgid "You must sign in to star a project" msgstr "Debes iniciar sesión para destacar un proyecto" @@ -797,10 +1040,10 @@ msgid "You will only receive notifications for threads you have participated in" msgstr "Solo recibirás notificaciones de los temas en los que has participado" msgid "You will receive notifications for any activity" -msgstr "Recibirás notificaciones para cualquier actividad" +msgstr "Recibirás notificaciones por cualquier actividad" msgid "You will receive notifications only for comments in which you were @mentioned" -msgstr "Recibirás notificaciones sólo para los comentarios en los que se te mencionó" +msgstr "Recibirás notificaciones solo para los comentarios en los que se te mencionó" msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account" msgstr "No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta" @@ -811,13 +1054,18 @@ msgstr "No podrás actualizar o enviar código al proyecto a través de SSH hast msgid "Your name" msgstr "Tu nombre" -msgid "committed" -msgstr "cambió" - msgid "day" msgid_plural "days" msgstr[0] "día" msgstr[1] "días" +msgid "new merge request" +msgstr "nueva solicitud de fusión" + msgid "notification emails" msgstr "correos electrónicos de notificación" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "padre" +msgstr[1] "padres" diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po new file mode 100644 index 00000000000..2000fa433b4 --- /dev/null +++ b/locale/fr/gitlab.po @@ -0,0 +1,207 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gitlab package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# Dremor <egeorget@opmbx.org>, 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-14 04:21-0400\n" +"Last-Translator: Dremor <egeorget@opmbx.org>\n" +"Language-Team: French (https://www.transifex.com/gitlab-fr/teams/75145/fr/)\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Zanata 3.9.6\n" + +msgid "ByAuthor|by" +msgstr "par" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Validation" +msgstr[1] "Validations" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgstr "L’analyseur de cycle permet d’avoir une vue d’ensemble du temps nécessaire pour aller d’une idée à sa mise en production pour votre projet." + +msgid "CycleAnalyticsStage|Code" +msgstr "Code" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Incident" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Planification" + +msgid "CycleAnalyticsStage|Production" +msgstr "Production" + +msgid "CycleAnalyticsStage|Review" +msgstr "Examen" + +msgid "CycleAnalyticsStage|Staging" +msgstr "Pré-production" + +msgid "CycleAnalyticsStage|Test" +msgstr "Test" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Déploiement" +msgstr[1] "Déploiements" + +msgid "FirstPushedBy|First" +msgstr "En premier" + +msgid "FirstPushedBy|pushed by" +msgstr "poussé par" + +msgid "From issue creation until deploy to production" +msgstr "Depuis la création de l'incident jusqu'au déploiement en production" + +msgid "From merge request merge until deploy to production" +msgstr "Depuis la fusion de la demande de fusion jusqu'au déploiement en production" + +msgid "Introducing Cycle Analytics" +msgstr "Introduction à l'analyseur de cycle" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "Le dernier %d jour" +msgstr[1] "Les derniers %d jours" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limiter l'affichage au plus à %d évènement" +msgstr[1] "Limiter l'affichage au plus à %d évènements" + +msgid "Median" +msgstr "Médian" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nouvel incident" +msgstr[1] "Nouveaux incidents" + +msgid "Not available" +msgstr "Indisponible" + +msgid "Not enough data" +msgstr "Données insuffisantes" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Ouvert" + +msgid "Pipeline Health" +msgstr "Santé du Pipeline" + +msgid "ProjectLifecycle|Stage" +msgstr "Étape" + +msgid "Read more" +msgstr "Lire plus" + +msgid "Related Commits" +msgstr "Validations liés" + +msgid "Related Deployed Jobs" +msgstr "Tâches de déploiement liés" + +msgid "Related Issues" +msgstr "Incidents liés" + +msgid "Related Jobs" +msgstr "Tâches liées" + +msgid "Related Merge Requests" +msgstr "Demandes de fusion liées" + +msgid "Related Merged Requests" +msgstr "Demandes fusionnées liées" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Affichage de %d évènement" +msgstr[1] "Affichage de %d évènements" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." +msgstr "L’étape de développement montre le temps entre la première validation et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "L’ensemble d’évènements ajoutés aux données récupérées pour cette étape." + +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." +msgstr "L'étape des incidents montre le temps nécessaire entre la création d'un incident et son assignation à un jalon, ou son ajout à une liste d'un tableau d'incident. Débutez à créer des incidents pour voir des données pour cette étape." + +msgid "The phase of the development lifecycle." +msgstr "Les étapes du cycle de développement." + +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." +msgstr "L’étape de planification montre le temps entre l’étape précédente et l’envoi de votre première validation. Ce temps sera automatiquement ajouté quand vous pousserez votre première validation." + +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgstr "L’étape de mise en production montre le temps nécessaire entre la création d’un incident et le déploiement du code en production. Les données seront automatiquement ajoutées une fois que vous aurez complété le cycle complet, depuis l’idée jusqu’à la mise en production." + +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." +msgstr "L’étape d’évaluation montre le temps entre la création de la demande de fusion et la fusion effective de celle-ci. Ces données seront automatiquement ajoutées après que vous ayez fusionné votre première demande de fusion." + +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." +msgstr "L’étape de pré-production indique le temps entre la fusion de la RF et le déploiement du code dans l’environnent de production. Les données seront automatiquement ajoutées une fois que vous déploierez en production pour la première fois." + +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." +msgstr "L’étape de test montre le temps que le CI de GitLab met pour exécuter chaque pipeline liés à la demande de fusion. Les données seront automatiquement ajoutées après que votre premier pipeline s’achèvera." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "Le temps pris par chaque entrée récoltée durant cette étape." + +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgstr "La valeur située au point médian d’une série de valeur observée. C.à.d., entre 3, 5, 9, le médian est 5. Entre 3, 5, 7, 8, le médian est (5+7)/2 = 6." + +msgid "Time before an issue gets scheduled" +msgstr "Temps avant qu’un incident ne soit planifié" + +msgid "Time before an issue starts implementation" +msgstr "Temps avant que résolution ne débute" + +msgid "Time between merge request creation and merge/close" +msgstr "Temps entre la création d'une demande de fusion et sa fusion/clôture" + +msgid "Time until first merge request" +msgstr "Temps jusqu’à la première demande de fusion" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "hr" +msgstr[1] "hrs" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "min" +msgstr[1] "mins" + +msgid "Time|s" +msgstr "s" + +msgid "Total Time" +msgstr "Temps total" + +msgid "Total test time for all commits/merges" +msgstr "Temps total de test pour toutes les validations/fusions" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "Vous voulez voir les données ? Merci de contacter un administrateur pour en obtenir l’accès." + +msgid "We don't have enough data to show this stage." +msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape." + +msgid "You need permission." +msgstr "Vous avez besoin d’une autorisation." + +msgid "day" +msgid_plural "days" +msgstr[0] "jour" +msgstr[1] "jours" diff --git a/locale/fr/gitlab.po.time_stamp b/locale/fr/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/fr/gitlab.po.time_stamp diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 050f6c446c1..07f9efeb495 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-07 21:22+0200\n" -"PO-Revision-Date: 2017-06-07 21:22+0200\n" +"POT-Creation-Date: 2017-06-19 15:50-0500\n" +"PO-Revision-Date: 2017-06-19 15:50-0500\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -18,23 +18,245 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" +msgid "%d additional commit have been omitted to prevent performance issues." +msgid_plural "%d additional commits have been omitted to prevent performance issues." +msgstr[0] "" +msgstr[1] "" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "" +msgstr[1] "" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "" + +msgid "About auto deploy" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "Activity" +msgstr "" + +msgid "Add Changelog" +msgstr "" + +msgid "Add Contribution guide" +msgstr "" + +msgid "Add License" +msgstr "" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "" + +msgid "Add new directory" +msgstr "" + +msgid "Archived project! Repository is read-only" +msgstr "" + msgid "Are you sure you want to delete this pipeline schedule?" msgstr "" +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "" +msgstr[1] "" + +msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" +msgstr "" + +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "" + +msgid "Branches" +msgstr "" + +msgid "Browse Directory" +msgstr "" + +msgid "Browse File" +msgstr "" + +msgid "Browse Files" +msgstr "" + +msgid "Browse files" +msgstr "" + msgid "ByAuthor|by" msgstr "" +msgid "CI configuration" +msgstr "" + msgid "Cancel" msgstr "" +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "" + +msgid "ChangeTypeAction|Revert" +msgstr "" + +msgid "Changelog" +msgstr "" + +msgid "Charts" +msgstr "" + +msgid "Cherry-pick this commit" +msgstr "" + +msgid "Cherry-pick this merge request" +msgstr "" + +msgid "CiStatusLabel|canceled" +msgstr "" + +msgid "CiStatusLabel|created" +msgstr "" + +msgid "CiStatusLabel|failed" +msgstr "" + +msgid "CiStatusLabel|manual action" +msgstr "" + +msgid "CiStatusLabel|passed" +msgstr "" + +msgid "CiStatusLabel|passed with warnings" +msgstr "" + +msgid "CiStatusLabel|pending" +msgstr "" + +msgid "CiStatusLabel|skipped" +msgstr "" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "" + +msgid "CiStatusText|blocked" +msgstr "" + +msgid "CiStatusText|canceled" +msgstr "" + +msgid "CiStatusText|created" +msgstr "" + +msgid "CiStatusText|failed" +msgstr "" + +msgid "CiStatusText|manual" +msgstr "" + +msgid "CiStatusText|passed" +msgstr "" + +msgid "CiStatusText|pending" +msgstr "" + +msgid "CiStatusText|skipped" +msgstr "" + +msgid "CiStatus|running" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit message" +msgstr "" + +msgid "CommitBoxTitle|Commit" +msgstr "" + +msgid "CommitMessage|Add %{file_name}" +msgstr "" + +msgid "Commits" +msgstr "" + +msgid "Commits feed" +msgstr "" + +msgid "Commits|History" +msgstr "" + +msgid "Committed by" +msgstr "" + +msgid "Compare" +msgstr "" + +msgid "Contribution guide" +msgstr "" + +msgid "Contributors" +msgstr "" + +msgid "Copy URL to clipboard" +msgstr "" + +msgid "Copy commit SHA to clipboard" +msgstr "" + +msgid "Create New Directory" +msgstr "" + +msgid "Create directory" +msgstr "" + +msgid "Create empty bare repository" +msgstr "" + +msgid "Create merge request" +msgstr "" + +msgid "Create new..." +msgstr "" + +msgid "CreateNewFork|Fork" +msgstr "" + +msgid "CreateTag|Tag" +msgstr "" + msgid "Cron Timezone" msgstr "" +msgid "Cron syntax" +msgstr "" + +msgid "Custom notification events" +msgstr "" + +msgid "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 %{notification_link}." +msgstr "" + +msgid "Cycle Analytics" +msgstr "" + msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" @@ -59,6 +281,9 @@ msgstr "" msgid "CycleAnalyticsStage|Test" msgstr "" +msgid "Define a custom pattern with cron syntax" +msgstr "" + msgid "Delete" msgstr "" @@ -70,19 +295,70 @@ msgstr[1] "" msgid "Description" msgstr "" +msgid "Directory name" +msgstr "" + +msgid "Don't show again" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Download tar" +msgstr "" + +msgid "Download tar.bz2" +msgstr "" + +msgid "Download tar.gz" +msgstr "" + +msgid "Download zip" +msgstr "" + +msgid "DownloadArtifacts|Download" +msgstr "" + +msgid "DownloadCommit|Email Patches" +msgstr "" + +msgid "DownloadCommit|Plain Diff" +msgstr "" + +msgid "DownloadSource|Download" +msgstr "" + msgid "Edit" msgstr "" msgid "Edit Pipeline Schedule %{id}" msgstr "" +msgid "Every day (at 4:00am)" +msgstr "" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "" + +msgid "Every week (Sundays at 4:00am)" +msgstr "" + msgid "Failed to change the owner" msgstr "" msgid "Failed to remove the pipeline schedule" msgstr "" -msgid "Filter" +msgid "Files" +msgstr "" + +msgid "Filter by commit message" +msgstr "" + +msgid "Find by path" +msgstr "" + +msgid "Find file" msgstr "" msgid "FirstPushedBy|First" @@ -91,18 +367,47 @@ msgstr "" msgid "FirstPushedBy|pushed by" msgstr "" +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "" +msgstr[1] "" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "" + msgid "From issue creation until deploy to production" msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "Go to your fork" +msgstr "" + +msgid "GoToYourFork|Fork" +msgstr "" + +msgid "Home" +msgstr "" + +msgid "Housekeeping successfully started" +msgstr "" + +msgid "Import repository" +msgstr "" + msgid "Interval Pattern" msgstr "" msgid "Introducing Cycle Analytics" msgstr "" +msgid "LFSStatus|Disabled" +msgstr "" + +msgid "LFSStatus|Enabled" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "" @@ -111,6 +416,24 @@ msgstr[1] "" msgid "Last Pipeline" msgstr "" +msgid "Last Update" +msgstr "" + +msgid "Last commit" +msgstr "" + +msgid "Learn more in the" +msgstr "" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "" + +msgid "Leave group" +msgstr "" + +msgid "Leave project" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "" @@ -119,6 +442,9 @@ msgstr[1] "" msgid "Median" msgstr "" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "" @@ -127,6 +453,33 @@ msgstr[1] "" msgid "New Pipeline Schedule" msgstr "" +msgid "New branch" +msgstr "" + +msgid "New directory" +msgstr "" + +msgid "New file" +msgstr "" + +msgid "New issue" +msgstr "" + +msgid "New merge request" +msgstr "" + +msgid "New schedule" +msgstr "" + +msgid "New snippet" +msgstr "" + +msgid "New tag" +msgstr "" + +msgid "No repository" +msgstr "" + msgid "No schedules" msgstr "" @@ -136,12 +489,75 @@ msgstr "" msgid "Not enough data" msgstr "" +msgid "Notification events" +msgstr "" + +msgid "NotificationEvent|Close issue" +msgstr "" + +msgid "NotificationEvent|Close merge request" +msgstr "" + +msgid "NotificationEvent|Failed pipeline" +msgstr "" + +msgid "NotificationEvent|Merge merge request" +msgstr "" + +msgid "NotificationEvent|New issue" +msgstr "" + +msgid "NotificationEvent|New merge request" +msgstr "" + +msgid "NotificationEvent|New note" +msgstr "" + +msgid "NotificationEvent|Reassign issue" +msgstr "" + +msgid "NotificationEvent|Reassign merge request" +msgstr "" + +msgid "NotificationEvent|Reopen issue" +msgstr "" + +msgid "NotificationEvent|Successful pipeline" +msgstr "" + +msgid "NotificationLevel|Custom" +msgstr "" + +msgid "NotificationLevel|Disabled" +msgstr "" + +msgid "NotificationLevel|Global" +msgstr "" + +msgid "NotificationLevel|On mention" +msgstr "" + +msgid "NotificationLevel|Participate" +msgstr "" + +msgid "NotificationLevel|Watch" +msgstr "" + +msgid "OfSearchInADropdown|Filter" +msgstr "" + msgid "OpenedNDaysAgo|Opened" msgstr "" +msgid "Options" +msgstr "" + msgid "Owner" msgstr "" +msgid "Pipeline" +msgstr "" + msgid "Pipeline Health" msgstr "" @@ -178,12 +594,78 @@ msgstr "" msgid "PipelineSchedules|Target" msgstr "" +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "" + +msgid "Pipeline|with stage" +msgstr "" + +msgid "Pipeline|with stages" +msgstr "" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "" + +msgid "Project '%{project_name}' was successfully created." +msgstr "" + +msgid "Project '%{project_name}' was successfully updated." +msgstr "" + +msgid "Project '%{project_name}' will be deleted." +msgstr "" + +msgid "Project access must be granted explicitly to each user." +msgstr "" + +msgid "Project export could not be deleted." +msgstr "" + +msgid "Project export has been deleted." +msgstr "" + +msgid "Project export link has expired. Please generate a new export from your project settings." +msgstr "" + +msgid "Project export started. A download link will be sent by email." +msgstr "" + +msgid "Project home" +msgstr "" + +msgid "ProjectFeature|Disabled" +msgstr "" + +msgid "ProjectFeature|Everyone with access" +msgstr "" + +msgid "ProjectFeature|Only team members" +msgstr "" + +msgid "ProjectFileTree|Name" +msgstr "" + +msgid "ProjectLastActivity|Never" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "" +msgid "ProjectNetworkGraph|Graph" +msgstr "" + msgid "Read more" msgstr "" +msgid "Readme" +msgstr "" + +msgid "RefSwitcher|Branches" +msgstr "" + +msgid "RefSwitcher|Tags" +msgstr "" + msgid "Related Commits" msgstr "" @@ -202,23 +684,82 @@ msgstr "" msgid "Related Merged Requests" msgstr "" +msgid "Remind later" +msgstr "" + +msgid "Remove project" +msgstr "" + +msgid "Request Access" +msgstr "" + +msgid "Revert this commit" +msgstr "" + +msgid "Revert this merge request" +msgstr "" + msgid "Save pipeline schedule" msgstr "" msgid "Schedule a new pipeline" msgstr "" +msgid "Scheduling Pipelines" +msgstr "" + +msgid "Search branches and tags" +msgstr "" + +msgid "Select Archive Format" +msgstr "" + msgid "Select a timezone" msgstr "" msgid "Select target branch" msgstr "" +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "" + +msgid "Set up CI" +msgstr "" + +msgid "Set up Koding" +msgstr "" + +msgid "Set up auto deploy" +msgstr "" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "" msgstr[1] "" +msgid "Source code" +msgstr "" + +msgid "StarProject|Star" +msgstr "" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "" + +msgid "Switch branch/tag" +msgstr "" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "" +msgstr[1] "" + +msgid "Tags" +msgstr "" + msgid "Target Branch" msgstr "" @@ -228,18 +769,33 @@ msgstr "" msgid "The collection of events added to the data gathered for that stage." msgstr "" +msgid "The fork relationship has been removed." +msgstr "" + msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "" msgid "The phase of the development lifecycle." msgstr "" +msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user." +msgstr "" + msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgstr "" msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgstr "" +msgid "The project can be accessed by any logged in user." +msgstr "" + +msgid "The project can be accessed without any authentication." +msgstr "" + +msgid "The repository for this project does not exist." +msgstr "" + msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "" @@ -255,6 +811,9 @@ msgstr "" msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." msgstr "" +msgid "This means you can not push code until you create an empty repository or import existing one." +msgstr "" + msgid "Time before an issue gets scheduled" msgstr "" @@ -267,6 +826,129 @@ msgstr "" msgid "Time until first merge request" msgstr "" +msgid "Timeago|%s days ago" +msgstr "" + +msgid "Timeago|%s days remaining" +msgstr "" + +msgid "Timeago|%s hours remaining" +msgstr "" + +msgid "Timeago|%s minutes ago" +msgstr "" + +msgid "Timeago|%s minutes remaining" +msgstr "" + +msgid "Timeago|%s months ago" +msgstr "" + +msgid "Timeago|%s months remaining" +msgstr "" + +msgid "Timeago|%s seconds remaining" +msgstr "" + +msgid "Timeago|%s weeks ago" +msgstr "" + +msgid "Timeago|%s weeks remaining" +msgstr "" + +msgid "Timeago|%s years ago" +msgstr "" + +msgid "Timeago|%s years remaining" +msgstr "" + +msgid "Timeago|1 day remaining" +msgstr "" + +msgid "Timeago|1 hour remaining" +msgstr "" + +msgid "Timeago|1 minute remaining" +msgstr "" + +msgid "Timeago|1 month remaining" +msgstr "" + +msgid "Timeago|1 week remaining" +msgstr "" + +msgid "Timeago|1 year remaining" +msgstr "" + +msgid "Timeago|Past due" +msgstr "" + +msgid "Timeago|a day ago" +msgstr "" + +msgid "Timeago|a month ago" +msgstr "" + +msgid "Timeago|a week ago" +msgstr "" + +msgid "Timeago|a while" +msgstr "" + +msgid "Timeago|a year ago" +msgstr "" + +msgid "Timeago|about %s hours ago" +msgstr "" + +msgid "Timeago|about a minute ago" +msgstr "" + +msgid "Timeago|about an hour ago" +msgstr "" + +msgid "Timeago|in %s days" +msgstr "" + +msgid "Timeago|in %s hours" +msgstr "" + +msgid "Timeago|in %s minutes" +msgstr "" + +msgid "Timeago|in %s months" +msgstr "" + +msgid "Timeago|in %s seconds" +msgstr "" + +msgid "Timeago|in %s weeks" +msgstr "" + +msgid "Timeago|in %s years" +msgstr "" + +msgid "Timeago|in 1 day" +msgstr "" + +msgid "Timeago|in 1 hour" +msgstr "" + +msgid "Timeago|in 1 minute" +msgstr "" + +msgid "Timeago|in 1 month" +msgstr "" + +msgid "Timeago|in 1 week" +msgstr "" + +msgid "Timeago|in 1 year" +msgstr "" + +msgid "Timeago|less than a minute ago" +msgstr "" + msgid "Time|hr" msgid_plural "Time|hrs" msgstr[0] "" @@ -286,16 +968,102 @@ msgstr "" msgid "Total test time for all commits/merges" msgstr "" +msgid "Unstar" +msgstr "" + +msgid "Upload New File" +msgstr "" + +msgid "Upload file" +msgstr "" + +msgid "UploadLink|click to upload" +msgstr "" + +msgid "Use your global notification setting" +msgstr "" + +msgid "View open merge request" +msgstr "" + +msgid "VisibilityLevel|Internal" +msgstr "" + +msgid "VisibilityLevel|Private" +msgstr "" + +msgid "VisibilityLevel|Public" +msgstr "" + msgid "Want to see the data? Please ask an administrator for access." msgstr "" msgid "We don't have enough data to show this stage." msgstr "" +msgid "Withdraw Access Request" +msgstr "" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" + +msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" + +msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?" +msgstr "" + +msgid "You can only add files when you are on a branch" +msgstr "" + +msgid "You have reached your project limit" +msgstr "" + +msgid "You must sign in to star a project" +msgstr "" + msgid "You need permission." msgstr "" +msgid "You will not get any notifications via email" +msgstr "" + +msgid "You will only receive notifications for the events you choose" +msgstr "" + +msgid "You will only receive notifications for threads you have participated in" +msgstr "" + +msgid "You will receive notifications for any activity" +msgstr "" + +msgid "You will receive notifications only for comments in which you were @mentioned" +msgstr "" + +msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account" +msgstr "" + +msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile" +msgstr "" + +msgid "Your name" +msgstr "" + msgid "day" msgid_plural "days" msgstr[0] "" msgstr[1] "" + +msgid "new merge request" +msgstr "" + +msgid "notification emails" +msgstr "" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "" +msgstr[1] "" diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po index 11434460207..8ba95093b82 100644 --- a/locale/zh_CN/gitlab.po +++ b/locale/zh_CN/gitlab.po @@ -1,39 +1,241 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the gitlab package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# +# Huang Tao <htve@outlook.com>, 2017. #zanata +# Xiaogang Wen <xiaogang@gitlab.com>, 2017. msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-05-04 19:24-0500\n" -"Last-Translator: HuangTao <htve@outlook.com>, 2017\n" -"Language-Team: Chinese (China) (https://www.transifex.com/gitlab-zh/teams/7517" -"7/zh_CN/)\n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: zh_CN\n" -"Plural-Forms: nplurals=1; plural=0;\n" +"PO-Revision-Date: 2017-06-19 09:57-0400\n" +"Last-Translator: Huang Tao <htve@outlook.com>\n" +"Language-Team: Chinese (China) (https://translate.zanata.org/project/view/GitLab)\n" +"Language: zh-CN\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "由 %{commit_author_link} 提交于 %{commit_timeago}" + +msgid "About auto deploy" +msgstr "关于自动部署" + +msgid "Active" +msgstr "启用" + +msgid "Activity" +msgstr "活动" + +msgid "Add Changelog" +msgstr "添加更新日志" + +msgid "Add Contribution guide" +msgstr "添加贡献指南" + +msgid "Add License" +msgstr "添加许可证" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "新建一个用于推送或拉取的 SSH 秘钥到账号中。" + +msgid "Add new directory" +msgstr "添加目录" + +msgid "Archived project! Repository is read-only" +msgstr "项目已归档!存储库为只读状态" msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "确定要删除此流水线计划吗?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "拖放文件到此处或者 %{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "分支" + +msgid "" +"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" msgstr "" +"已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择合适的 GitLab CI Yaml " +"模板并提交更改。%{link_to_autodeploy_doc}" + +msgid "Branches" +msgstr "分支" + +msgid "Browse files" +msgstr "浏览文件" msgid "ByAuthor|by" msgstr "作者:" +msgid "CI configuration" +msgstr "CI 配置" + msgid "Cancel" -msgstr "" +msgstr "取消" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "选择分支" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "还原分支" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "优选" + +msgid "ChangeTypeAction|Revert" +msgstr "还原" + +msgid "Changelog" +msgstr "更新日志" + +msgid "Charts" +msgstr "统计图" + +msgid "Cherry-pick this commit" +msgstr "优选此提交" + +msgid "Cherry-pick this merge request" +msgstr "优选此合并请求" + +msgid "CiStatusLabel|canceled" +msgstr "已取消" + +msgid "CiStatusLabel|created" +msgstr "已创建" + +msgid "CiStatusLabel|failed" +msgstr "已失败" + +msgid "CiStatusLabel|manual action" +msgstr "手动操作" + +msgid "CiStatusLabel|passed" +msgstr "已通过" + +msgid "CiStatusLabel|passed with warnings" +msgstr "已通过但有警告" + +msgid "CiStatusLabel|pending" +msgstr "等待中" + +msgid "CiStatusLabel|skipped" +msgstr "已跳过" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "等待手动操作" + +msgid "CiStatusText|blocked" +msgstr "已阻塞" + +msgid "CiStatusText|canceled" +msgstr "已取消" + +msgid "CiStatusText|created" +msgstr "已创建" + +msgid "CiStatusText|failed" +msgstr "已失败" + +msgid "CiStatusText|manual" +msgstr "手动操作" + +msgid "CiStatusText|passed" +msgstr "已通过" + +msgid "CiStatusText|pending" +msgstr "等待中" + +msgid "CiStatusText|skipped" +msgstr "已跳过" + +msgid "CiStatus|running" +msgstr "运行中" msgid "Commit" msgid_plural "Commits" msgstr[0] "提交" +msgid "Commit message" +msgstr "提交信息" + +msgid "CommitBoxTitle|Commit" +msgstr "提交" + +msgid "CommitMessage|Add %{file_name}" +msgstr "添加 %{file_name}" + +msgid "Commits" +msgstr "提交" + +msgid "Commits|History" +msgstr "历史" + +msgid "Committed by" +msgstr "提交者:" + +msgid "Compare" +msgstr "比较" + +msgid "Contribution guide" +msgstr "贡献指南" + +msgid "Contributors" +msgstr "贡献者" + +msgid "Copy URL to clipboard" +msgstr "复制 URL 到剪贴板" + +msgid "Copy commit SHA to clipboard" +msgstr "复制提交 SHA 的值到剪贴板" + +msgid "Create New Directory" +msgstr "创建新目录" + +msgid "Create directory" +msgstr "创建目录" + +msgid "Create empty bare repository" +msgstr "创建空的存储库" + +msgid "Create merge request" +msgstr "创建合并请求" + +msgid "Create new..." +msgstr "创建..." + +msgid "CreateNewFork|Fork" +msgstr "派生" + +msgid "CreateTag|Tag" +msgstr "标签" + msgid "Cron Timezone" +msgstr "Cron 时区" + +msgid "Cron syntax" +msgstr "Cron 语法" + +msgid "Custom notification events" +msgstr "自定义通知事件" + +msgid "" +"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 %{notification_link}." msgstr "" +"自定义通知级别继承自参与级别。使用自定义通知级别,您会收到参与级别及选定事件的通知。想了解更多信息,请查看 %{notification_link}." + +msgid "Cycle Analytics" +msgstr "周期分析" -msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." msgstr "周期分析概述了项目从想法到产品实现的各阶段所需的时间。" msgid "CycleAnalyticsStage|Code" @@ -57,30 +259,81 @@ msgstr "预发布" msgid "CycleAnalyticsStage|Test" msgstr "测试" +msgid "Define a custom pattern with cron syntax" +msgstr "使用 Cron 语法定义自定义模式" + msgid "Delete" -msgstr "" +msgstr "删除" msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" msgid "Description" -msgstr "" +msgstr "描述" + +msgid "Directory name" +msgstr "目录名称" + +msgid "Don't show again" +msgstr "不再显示" + +msgid "Download" +msgstr "下载" + +msgid "Download tar" +msgstr "下载 tar" + +msgid "Download tar.bz2" +msgstr "下载 tar.bz2" + +msgid "Download tar.gz" +msgstr "下载 tar.gz" + +msgid "Download zip" +msgstr "下载 zip" + +msgid "DownloadArtifacts|Download" +msgstr "下载" + +msgid "DownloadCommit|Email Patches" +msgstr "电子邮件补丁" + +msgid "DownloadCommit|Plain Diff" +msgstr "差异文件" + +msgid "DownloadSource|Download" +msgstr "下载" msgid "Edit" -msgstr "" +msgstr "编辑" msgid "Edit Pipeline Schedule %{id}" -msgstr "" +msgstr "编辑 %{id} 流水线计划" + +msgid "Every day (at 4:00am)" +msgstr "每日执行(凌晨 4 点)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "每月执行(每月 1 日凌晨 4 点)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "每周执行(周日凌晨 4 点)" msgid "Failed to change the owner" -msgstr "" +msgstr "无法变更所有者" msgid "Failed to remove the pipeline schedule" -msgstr "" +msgstr "无法删除流水线计划" -msgid "Filter" -msgstr "" +msgid "Files" +msgstr "文件" + +msgid "Find by path" +msgstr "按路径查找" + +msgid "Find file" +msgstr "查找文件" msgid "FirstPushedBy|First" msgstr "首次推送" @@ -88,24 +341,70 @@ msgstr "首次推送" msgid "FirstPushedBy|pushed by" msgstr "推送者:" +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "派生" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "派生自" + msgid "From issue creation until deploy to production" msgstr "从创建议题到部署至生产环境" msgid "From merge request merge until deploy to production" msgstr "从合并请求被合并后到部署至生产环境" +msgid "Go to your fork" +msgstr "跳转到派生项目" + +msgid "GoToYourFork|Fork" +msgstr "跳转到派生项目" + +msgid "Home" +msgstr "首页" + +msgid "Housekeeping successfully started" +msgstr "已开始维护" + +msgid "Import repository" +msgstr "导入存储库" + msgid "Interval Pattern" -msgstr "" +msgstr "循环周期" msgid "Introducing Cycle Analytics" msgstr "周期分析简介" +msgid "LFSStatus|Disabled" +msgstr "停用" + +msgid "LFSStatus|Enabled" +msgstr "启用" + msgid "Last %d day" msgid_plural "Last %d days" -msgstr[0] "最后 %d 天" +msgstr[0] "最近 %d 天" msgid "Last Pipeline" -msgstr "" +msgstr "最新流水线" + +msgid "Last Update" +msgstr "最后更新" + +msgid "Last commit" +msgstr "最后提交" + +msgid "Learn more in the" +msgstr "了解更多" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "流水线计划文档" + +msgid "Leave group" +msgstr "退出群组" + +msgid "Leave project" +msgstr "退出项目" msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" @@ -114,15 +413,45 @@ msgstr[0] "最多显示 %d 个事件" msgid "Median" msgstr "中位数" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "新建 SSH 公钥" + msgid "New Issue" msgid_plural "New Issues" -msgstr[0] "新议题" +msgstr[0] "新建议题" msgid "New Pipeline Schedule" -msgstr "" +msgstr "创建流水线计划" + +msgid "New branch" +msgstr "新建分支" + +msgid "New directory" +msgstr "新建目录" + +msgid "New file" +msgstr "新建文件" + +msgid "New issue" +msgstr "新建议题" + +msgid "New merge request" +msgstr "新建合并请求" + +msgid "New schedule" +msgstr "新建计划" + +msgid "New snippet" +msgstr "新建代码片段" + +msgid "New tag" +msgstr "新建标签" + +msgid "No repository" +msgstr "没有存储库" msgid "No schedules" -msgstr "" +msgstr "没有计划" msgid "Not available" msgstr "数据不足" @@ -130,54 +459,185 @@ msgstr "数据不足" msgid "Not enough data" msgstr "数据不足" +msgid "Notification events" +msgstr "通知事件" + +msgid "NotificationEvent|Close issue" +msgstr "关闭议题" + +msgid "NotificationEvent|Close merge request" +msgstr "关闭合并请求" + +msgid "NotificationEvent|Failed pipeline" +msgstr "流水线失败" + +msgid "NotificationEvent|Merge merge request" +msgstr "合并请求被合并" + +msgid "NotificationEvent|New issue" +msgstr "新建议题" + +msgid "NotificationEvent|New merge request" +msgstr "新建合并请求" + +msgid "NotificationEvent|New note" +msgstr "新建评论" + +msgid "NotificationEvent|Reassign issue" +msgstr "重新指派议题" + +msgid "NotificationEvent|Reassign merge request" +msgstr "重新指派合并请求" + +msgid "NotificationEvent|Reopen issue" +msgstr "重启议题" + +msgid "NotificationEvent|Successful pipeline" +msgstr "流水线成功完成" + +msgid "NotificationLevel|Custom" +msgstr "自定义" + +msgid "NotificationLevel|Disabled" +msgstr "停用" + +msgid "NotificationLevel|Global" +msgstr "全局" + +msgid "NotificationLevel|On mention" +msgstr "提及" + +msgid "NotificationLevel|Participate" +msgstr "参与" + +msgid "NotificationLevel|Watch" +msgstr "关注" + +msgid "OfSearchInADropdown|Filter" +msgstr "筛选" + msgid "OpenedNDaysAgo|Opened" msgstr "开始于" +msgid "Options" +msgstr "操作" + msgid "Owner" -msgstr "" +msgstr "所有者" + +msgid "Pipeline" +msgstr "流水线" msgid "Pipeline Health" msgstr "流水线健康指标" msgid "Pipeline Schedule" -msgstr "" +msgstr "流水线计划" msgid "Pipeline Schedules" -msgstr "" +msgstr "流水线计划" msgid "PipelineSchedules|Activated" -msgstr "" +msgstr "是否启用" msgid "PipelineSchedules|Active" -msgstr "" +msgstr "已启用" msgid "PipelineSchedules|All" -msgstr "" +msgstr "所有" msgid "PipelineSchedules|Inactive" -msgstr "" +msgstr "未启用" msgid "PipelineSchedules|Next Run" -msgstr "" +msgstr "下次运行时间" msgid "PipelineSchedules|None" -msgstr "" +msgstr "无" msgid "PipelineSchedules|Provide a short description for this pipeline" -msgstr "" +msgstr "为此流水线提供简短描述" msgid "PipelineSchedules|Take ownership" -msgstr "" +msgstr "取得所有者" msgid "PipelineSchedules|Target" -msgstr "" +msgstr "目标" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "自定义" + +msgid "Pipeline|with stage" +msgstr "于阶段" + +msgid "Pipeline|with stages" +msgstr "于阶段" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "项目 '%{project_name}' 已进入删除队列。" + +msgid "Project '%{project_name}' was successfully created." +msgstr "项目 '%{project_name}' 已创建成功。" + +msgid "Project '%{project_name}' was successfully updated." +msgstr "项目 '%{project_name}' 已更新完成。" + +msgid "Project '%{project_name}' will be deleted." +msgstr "项目 '%{project_name}' 将被删除。" + +msgid "Project access must be granted explicitly to each user." +msgstr "项目访问权限必须明确授权给每个用户。" + +msgid "Project export could not be deleted." +msgstr "无法删除项目导出。" + +msgid "Project export has been deleted." +msgstr "项目导出已被删除。" + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "项目导出链接已过期。请从项目设置中重新生成项目导出。" + +msgid "Project export started. A download link will be sent by email." +msgstr "项目导出已开始。下载链接将通过电子邮件发送。" + +msgid "Project home" +msgstr "项目首页" + +msgid "ProjectFeature|Disabled" +msgstr "停用" + +msgid "ProjectFeature|Everyone with access" +msgstr "任何对项目有访问权的人" + +msgid "ProjectFeature|Only team members" +msgstr "只限团队成员" + +msgid "ProjectFileTree|Name" +msgstr "名称" + +msgid "ProjectLastActivity|Never" +msgstr "从未" msgid "ProjectLifecycle|Stage" -msgstr "项目生命周期" +msgstr "阶段" + +msgid "ProjectNetworkGraph|Graph" +msgstr "分支图" msgid "Read more" msgstr "了解更多" +msgid "Readme" +msgstr "自述文件" + +msgid "RefSwitcher|Branches" +msgstr "分支" + +msgid "RefSwitcher|Tags" +msgstr "标签" + msgid "Related Commits" msgstr "相关的提交" @@ -196,58 +656,163 @@ msgstr "相关的合并请求" msgid "Related Merged Requests" msgstr "相关已合并的合并请求" +msgid "Remind later" +msgstr "稍后提醒" + +msgid "Remove project" +msgstr "删除项目" + +msgid "Request Access" +msgstr "申请权限" + +msgid "Revert this commit" +msgstr "还原此提交" + +msgid "Revert this merge request" +msgstr "还原此合并请求" + msgid "Save pipeline schedule" -msgstr "" +msgstr "保存流水线计划" msgid "Schedule a new pipeline" -msgstr "" +msgstr "新建流水线计划" + +msgid "Scheduling Pipelines" +msgstr "流水线计划" + +msgid "Search branches and tags" +msgstr "搜索分支和标签" + +msgid "Select Archive Format" +msgstr "选择下载格式" msgid "Select a timezone" -msgstr "" +msgstr "选择时区" msgid "Select target branch" -msgstr "" +msgstr "选择目标分支" + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "为账号创建一个用于推送或拉取的 %{protocol} 密码。" + +msgid "Set up CI" +msgstr "设置 CI" + +msgid "Set up Koding" +msgstr "设置 Koding" + +msgid "Set up auto deploy" +msgstr "设置自动部署" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "设置密码" msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "显示 %d 个事件" +msgid "Source code" +msgstr "源代码" + +msgid "StarProject|Star" +msgstr "星标" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "由此更改 %{new_merge_request}" + +msgid "Switch branch/tag" +msgstr "切换分支/标签" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "标签" + +msgid "Tags" +msgstr "标签" + msgid "Target Branch" -msgstr "" +msgstr "目标分支" -msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." msgstr "编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。" msgid "The collection of events added to the data gathered for that stage." -msgstr "与该阶段相关的事件。" +msgstr "与该阶段相关的事件集合。" -msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." -msgstr "议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。" +msgid "The fork relationship has been removed." +msgstr "派生关系已被删除。" + +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "议题阶段概述了从创建议题到将议题添加到里程碑或议题看板所花费的时间。创建第一个议题后,数据将自动添加到此处.。" msgid "The phase of the development lifecycle." msgstr "项目生命周期中的各个阶段。" -msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." -msgstr "计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。" +msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "流水线计划会周期性重复运行指定分支或标签的流水线。这些流水线将根据其关联用户继承有限的项目访问权限。" -msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "计划阶段概述了从议题添加到日程到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。" + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." msgstr "生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。" -msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." +msgid "The project can be accessed by any logged in user." +msgstr "该项目允许已登录的用户访问。" + +msgid "The project can be accessed without any authentication." +msgstr "该项目允许任何人访问。" + +msgid "The repository for this project does not exist." +msgstr "此项目的存储库不存在。" + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." msgstr "评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。" -msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." msgstr "预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。" -msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." -msgstr "测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。" +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "测试阶段概述了 GitLab CI 为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。" msgid "The time taken by each data entry gathered by that stage." msgstr "该阶段每条数据所花的时间" -msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." msgstr "中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。" +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "在创建一个空的存储库或导入现有存储库之前,将无法推送代码。" + msgid "Time before an issue gets scheduled" msgstr "议题被列入日程表的时间" @@ -260,6 +825,129 @@ msgstr "从创建合并请求到被合并或关闭的时间" msgid "Time until first merge request" msgstr "创建第一个合并请求之前的时间" +msgid "Timeago|%s days ago" +msgstr " %s 天前" + +msgid "Timeago|%s days remaining" +msgstr "剩余 %s 天" + +msgid "Timeago|%s hours remaining" +msgstr "剩余 %s 小时" + +msgid "Timeago|%s minutes ago" +msgstr " %s 分钟前" + +msgid "Timeago|%s minutes remaining" +msgstr "剩余 %s 分钟" + +msgid "Timeago|%s months ago" +msgstr " %s 个月前" + +msgid "Timeago|%s months remaining" +msgstr "剩余 %s 月" + +msgid "Timeago|%s seconds remaining" +msgstr "剩余 %s 秒" + +msgid "Timeago|%s weeks ago" +msgstr " %s 星期前" + +msgid "Timeago|%s weeks remaining" +msgstr "剩余 %s 星期" + +msgid "Timeago|%s years ago" +msgstr " %s 年前" + +msgid "Timeago|%s years remaining" +msgstr "剩余 %s 年" + +msgid "Timeago|1 day remaining" +msgstr "剩余 1 天" + +msgid "Timeago|1 hour remaining" +msgstr "剩余 1 小时" + +msgid "Timeago|1 minute remaining" +msgstr "剩余 1 分钟" + +msgid "Timeago|1 month remaining" +msgstr "剩余 1 个月" + +msgid "Timeago|1 week remaining" +msgstr "剩余 1 星期" + +msgid "Timeago|1 year remaining" +msgstr "剩余 1 年" + +msgid "Timeago|Past due" +msgstr "逾期" + +msgid "Timeago|a day ago" +msgstr " 1 天前" + +msgid "Timeago|a month ago" +msgstr " 1 个月前" + +msgid "Timeago|a week ago" +msgstr " 1 星期前" + +msgid "Timeago|a while" +msgstr "刚刚" + +msgid "Timeago|a year ago" +msgstr " 1 年前" + +msgid "Timeago|about %s hours ago" +msgstr "约 %s 小时前" + +msgid "Timeago|about a minute ago" +msgstr "约 1 分钟前" + +msgid "Timeago|about an hour ago" +msgstr "约 1 小时前" + +msgid "Timeago|in %s days" +msgstr " %s 天后" + +msgid "Timeago|in %s hours" +msgstr " %s 小时后" + +msgid "Timeago|in %s minutes" +msgstr " %s 分钟后" + +msgid "Timeago|in %s months" +msgstr " %s 个月后" + +msgid "Timeago|in %s seconds" +msgstr " %s 秒后" + +msgid "Timeago|in %s weeks" +msgstr " %s 星期后" + +msgid "Timeago|in %s years" +msgstr " %s 年后" + +msgid "Timeago|in 1 day" +msgstr " 1 天后" + +msgid "Timeago|in 1 hour" +msgstr " 1 小时后" + +msgid "Timeago|in 1 minute" +msgstr " 1 分钟后" + +msgid "Timeago|in 1 month" +msgstr " 1 月后" + +msgid "Timeago|in 1 week" +msgstr " 1 星期后" + +msgid "Timeago|in 1 year" +msgstr " 1 年后" + +msgid "Timeago|less than a minute ago" +msgstr "不到 1 分钟前" + msgid "Time|hr" msgid_plural "Time|hrs" msgstr[0] "小时" @@ -277,15 +965,108 @@ msgstr "总时间" msgid "Total test time for all commits/merges" msgstr "所有提交和合并的总测试时间" +msgid "Unstar" +msgstr "取消星标" + +msgid "Upload New File" +msgstr "上传新文件" + +msgid "Upload file" +msgstr "上传文件" + +msgid "Use your global notification setting" +msgstr "使用全局通知设置" + +msgid "VisibilityLevel|Internal" +msgstr "内部" + +msgid "VisibilityLevel|Private" +msgstr "私有" + +msgid "VisibilityLevel|Public" +msgstr "公开" + msgid "Want to see the data? Please ask an administrator for access." msgstr "权限不足。如需查看相关数据,请向管理员申请权限。" msgid "We don't have enough data to show this stage." msgstr "该阶段的数据不足,无法显示。" +msgid "Withdraw Access Request" +msgstr "取消权限申请" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "即将要删除 %{project_name_with_namespace}。\n" +"已删除的项目无法恢复!\n" +"确定继续吗?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "即将删除与源项目 %{forked_from_project} 的派生关系。确定继续吗?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "即将 %{project_name_with_namespace} 转移给另一个所有者。确定继续吗?" + +msgid "You can only add files when you are on a branch" +msgstr "只能在分支上添加文件" + +msgid "You have reached your project limit" +msgstr "您已达到项目数量限制" + +msgid "You must sign in to star a project" +msgstr "必须登录才能对项目加星标" + msgid "You need permission." -msgstr "您需要相关的权限。" +msgstr "需要相关的权限。" + +msgid "You will not get any notifications via email" +msgstr "不会收到任何通知邮件" + +msgid "You will only receive notifications for the events you choose" +msgstr "只接收选择的事件通知" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "只接收参与的主题的通知" + +msgid "You will receive notifications for any activity" +msgstr "接收所有活动的通知" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "只接收评论中提及(@)您的通知" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "在账号中 %{set_password_link} 之前将无法通过 %{protocol} 拉取或推送代码。" + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "在账号中 %{add_ssh_key_link} 之前将无法通过 SSH 拉取或推送代码。" + +msgid "Your name" +msgstr "您的名字" msgid "day" msgid_plural "days" msgstr[0] "天" + +msgid "new merge request" +msgstr "新建合并请求" + +msgid "notification emails" +msgstr "通知邮件" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "父级" + diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index 81b2ff863ea..4d545d27185 100644 --- a/locale/zh_HK/gitlab.po +++ b/locale/zh_HK/gitlab.po @@ -1,39 +1,242 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the gitlab package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# +# Huang Tao <htve@outlook.com>, 2017. #zanata +# Victor Wu <anonymous@domain.com>, 2017. +# Hazel Yang <anonymous@domain.com>, 2017. msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-05-04 19:24-0500\n" -"Last-Translator: HuangTao <htve@outlook.com>, 2017\n" -"Language-Team: Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/" -"75177/zh_HK/)\n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: zh_HK\n" -"Plural-Forms: nplurals=1; plural=0;\n" +"PO-Revision-Date: 2017-06-19 09:57-0400\n" +"Last-Translator: Huang Tao <htve@outlook.com>\n" +"Language-Team: Chinese (Hong Kong) (https://translate.zanata.org/project/view/GitLab)\n" +"Language: zh-HK\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=1; plural=0\n" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "由 %{commit_author_link} 提交於 %{commit_timeago}" + +msgid "About auto deploy" +msgstr "關於自動部署" + +msgid "Active" +msgstr "啟用" + +msgid "Activity" +msgstr "活動" + +msgid "Add Changelog" +msgstr "添加更新日誌" + +msgid "Add Contribution guide" +msgstr "添加貢獻指南" + +msgid "Add License" +msgstr "添加許可證" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "新增壹個用於推送或拉取的 SSH 秘鑰到賬號中。" + +msgid "Add new directory" +msgstr "添加新目錄" + +msgid "Archived project! Repository is read-only" +msgstr "歸檔項目!存儲庫為只讀" msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "確定要刪除此流水線計劃嗎?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "拖放文件到此處或者 %{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "分支" + +msgid "" +"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" msgstr "" +"分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部署, 請選擇合適的 GitLab CI Yaml " +"模板併提交更改。%{link_to_autodeploy_doc}" + +msgid "Branches" +msgstr "分支" + +msgid "Browse files" +msgstr "瀏覽文件" msgid "ByAuthor|by" msgstr "作者:" +msgid "CI configuration" +msgstr "CI 配置" + msgid "Cancel" -msgstr "" +msgstr "取消" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "挑選到分支" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "還原分支" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "優選" + +msgid "ChangeTypeAction|Revert" +msgstr "還原" + +msgid "Changelog" +msgstr "更新日誌" + +msgid "Charts" +msgstr "統計圖" + +msgid "Cherry-pick this commit" +msgstr "優選此提交" + +msgid "Cherry-pick this merge request" +msgstr "優選此合併請求" + +msgid "CiStatusLabel|canceled" +msgstr "已取消" + +msgid "CiStatusLabel|created" +msgstr "已創建" + +msgid "CiStatusLabel|failed" +msgstr "已失敗" + +msgid "CiStatusLabel|manual action" +msgstr "手動操作" + +msgid "CiStatusLabel|passed" +msgstr "已通過" + +msgid "CiStatusLabel|passed with warnings" +msgstr "已通過但有警告" + +msgid "CiStatusLabel|pending" +msgstr "等待中" + +msgid "CiStatusLabel|skipped" +msgstr "已跳過" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "等待手動操作" + +msgid "CiStatusText|blocked" +msgstr "已阻塞" + +msgid "CiStatusText|canceled" +msgstr "已取消" + +msgid "CiStatusText|created" +msgstr "已創建" + +msgid "CiStatusText|failed" +msgstr "已失敗" + +msgid "CiStatusText|manual" +msgstr "待手動" + +msgid "CiStatusText|passed" +msgstr "已通過" + +msgid "CiStatusText|pending" +msgstr "等待中" + +msgid "CiStatusText|skipped" +msgstr "已跳過" + +msgid "CiStatus|running" +msgstr "運行中" msgid "Commit" msgid_plural "Commits" msgstr[0] "提交" +msgid "Commit message" +msgstr "提交信息" + +msgid "CommitBoxTitle|Commit" +msgstr "提交" + +msgid "CommitMessage|Add %{file_name}" +msgstr "添加 %{file_name}" + +msgid "Commits" +msgstr "提交" + +msgid "Commits|History" +msgstr "歷史" + +msgid "Committed by" +msgstr "提交者:" + +msgid "Compare" +msgstr "比較" + +msgid "Contribution guide" +msgstr "貢獻指南" + +msgid "Contributors" +msgstr "貢獻者" + +msgid "Copy URL to clipboard" +msgstr "複製URL到剪貼板" + +msgid "Copy commit SHA to clipboard" +msgstr "複製提交 SHA 到剪貼板" + +msgid "Create New Directory" +msgstr "創建新目錄" + +msgid "Create directory" +msgstr "創建目錄" + +msgid "Create empty bare repository" +msgstr "創建空的存儲庫" + +msgid "Create merge request" +msgstr "創建合併請求" + +msgid "Create new..." +msgstr "創建..." + +msgid "CreateNewFork|Fork" +msgstr "派生" + +msgid "CreateTag|Tag" +msgstr "標籤" + msgid "Cron Timezone" +msgstr "Cron 時區" + +msgid "Cron syntax" +msgstr "Cron 語法" + +msgid "Custom notification events" +msgstr "自定義通知事件" + +msgid "" +"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 %{notification_link}." msgstr "" +"自定義通知級別繼承自參與級別。使用自定義通知級別,您會收到參與級別及選定事件的通知。想了解更多信息,請查看 %{notification_link}." + +msgid "Cycle Analytics" +msgstr "週期分析" -msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." msgstr "週期分析概述了項目從想法到產品實現的各階段所需的時間。" msgid "CycleAnalyticsStage|Code" @@ -57,30 +260,81 @@ msgstr "預發布" msgid "CycleAnalyticsStage|Test" msgstr "測試" +msgid "Define a custom pattern with cron syntax" +msgstr "使用 Cron 語法定義自定義模式" + msgid "Delete" -msgstr "" +msgstr "刪除" msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" msgid "Description" -msgstr "" +msgstr "描述" + +msgid "Directory name" +msgstr "目錄名稱" + +msgid "Don't show again" +msgstr "不再顯示" + +msgid "Download" +msgstr "下載" + +msgid "Download tar" +msgstr "下載 tar" + +msgid "Download tar.bz2" +msgstr "下載 tar.bz2" + +msgid "Download tar.gz" +msgstr "下載 tar.gz" + +msgid "Download zip" +msgstr "下載 zip" + +msgid "DownloadArtifacts|Download" +msgstr "下載" + +msgid "DownloadCommit|Email Patches" +msgstr "電子郵件補丁" + +msgid "DownloadCommit|Plain Diff" +msgstr "差異文件" + +msgid "DownloadSource|Download" +msgstr "下載" msgid "Edit" -msgstr "" +msgstr "編輯" msgid "Edit Pipeline Schedule %{id}" -msgstr "" +msgstr "編輯 %{id} 流水線計劃" + +msgid "Every day (at 4:00am)" +msgstr "每日執行(淩晨 4 點)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "每月執行(每月 1 日淩晨 4 點)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "每週執行(周日淩晨 4 點)" msgid "Failed to change the owner" -msgstr "" +msgstr "無法變更所有者" msgid "Failed to remove the pipeline schedule" -msgstr "" +msgstr "無法刪除流水線計劃" -msgid "Filter" -msgstr "" +msgid "Files" +msgstr "文件" + +msgid "Find by path" +msgstr "按路徑查找" + +msgid "Find file" +msgstr "查找文件" msgid "FirstPushedBy|First" msgstr "首次推送" @@ -88,24 +342,70 @@ msgstr "首次推送" msgid "FirstPushedBy|pushed by" msgstr "推送者:" +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "派生" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "派生自" + msgid "From issue creation until deploy to production" msgstr "從創建議題到部署到生產環境" msgid "From merge request merge until deploy to production" msgstr "從合併請求的合併到部署至生產環境" +msgid "Go to your fork" +msgstr "跳轉到派生項目" + +msgid "GoToYourFork|Fork" +msgstr "跳轉到派生項目" + +msgid "Home" +msgstr "首頁" + +msgid "Housekeeping successfully started" +msgstr "已開始維護" + +msgid "Import repository" +msgstr "導入存儲庫" + msgid "Interval Pattern" -msgstr "" +msgstr "循環週期" msgid "Introducing Cycle Analytics" msgstr "週期分析簡介" +msgid "LFSStatus|Disabled" +msgstr "停用" + +msgid "LFSStatus|Enabled" +msgstr "啟用" + msgid "Last %d day" msgid_plural "Last %d days" -msgstr[0] "最後 %d 天" +msgstr[0] "最近 %d 天" msgid "Last Pipeline" -msgstr "" +msgstr "最新流水線" + +msgid "Last Update" +msgstr "最後更新" + +msgid "Last commit" +msgstr "最後提交" + +msgid "Learn more in the" +msgstr "了解更多" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "流水線計劃文檔" + +msgid "Leave group" +msgstr "退出群組" + +msgid "Leave project" +msgstr "退出項目" msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" @@ -114,15 +414,45 @@ msgstr[0] "最多顯示 %d 個事件" msgid "Median" msgstr "中位數" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "添加壹個 SSH 公鑰" + msgid "New Issue" msgid_plural "New Issues" -msgstr[0] "新議題" +msgstr[0] "新建議題" msgid "New Pipeline Schedule" -msgstr "" +msgstr "創建流水線計劃" + +msgid "New branch" +msgstr "新增分支" + +msgid "New directory" +msgstr "新增目錄" + +msgid "New file" +msgstr "新增文件" + +msgid "New issue" +msgstr "新議題" + +msgid "New merge request" +msgstr "新增合併請求" + +msgid "New schedule" +msgstr "新增计划" + +msgid "New snippet" +msgstr "新代碼片段" + +msgid "New tag" +msgstr "新增標籤" + +msgid "No repository" +msgstr "沒有存儲庫" msgid "No schedules" -msgstr "" +msgstr "沒有計劃" msgid "Not available" msgstr "不可用" @@ -130,54 +460,185 @@ msgstr "不可用" msgid "Not enough data" msgstr "數據不足" +msgid "Notification events" +msgstr "通知事件" + +msgid "NotificationEvent|Close issue" +msgstr "關閉議題" + +msgid "NotificationEvent|Close merge request" +msgstr "關閉合併請求" + +msgid "NotificationEvent|Failed pipeline" +msgstr "流水線失敗" + +msgid "NotificationEvent|Merge merge request" +msgstr "合併請求被合併" + +msgid "NotificationEvent|New issue" +msgstr "新增議題" + +msgid "NotificationEvent|New merge request" +msgstr "新合併請求" + +msgid "NotificationEvent|New note" +msgstr "新增評論" + +msgid "NotificationEvent|Reassign issue" +msgstr "重新指派議題" + +msgid "NotificationEvent|Reassign merge request" +msgstr "重新指派合併請求" + +msgid "NotificationEvent|Reopen issue" +msgstr "重啟議題" + +msgid "NotificationEvent|Successful pipeline" +msgstr "流水線成功完成" + +msgid "NotificationLevel|Custom" +msgstr "自定義" + +msgid "NotificationLevel|Disabled" +msgstr "停用" + +msgid "NotificationLevel|Global" +msgstr "全局" + +msgid "NotificationLevel|On mention" +msgstr "提及" + +msgid "NotificationLevel|Participate" +msgstr "參與" + +msgid "NotificationLevel|Watch" +msgstr "關注" + +msgid "OfSearchInADropdown|Filter" +msgstr "篩選" + msgid "OpenedNDaysAgo|Opened" msgstr "開始於" +msgid "Options" +msgstr "操作" + msgid "Owner" -msgstr "" +msgstr "所有者" + +msgid "Pipeline" +msgstr "流水線" msgid "Pipeline Health" msgstr "流水線健康指標" msgid "Pipeline Schedule" -msgstr "" +msgstr "流水線計劃" msgid "Pipeline Schedules" -msgstr "" +msgstr "流水線計劃" msgid "PipelineSchedules|Activated" -msgstr "" +msgstr "是否啟用" msgid "PipelineSchedules|Active" -msgstr "" +msgstr "已啟用" msgid "PipelineSchedules|All" -msgstr "" +msgstr "所有" msgid "PipelineSchedules|Inactive" -msgstr "" +msgstr "未啟用" msgid "PipelineSchedules|Next Run" -msgstr "" +msgstr "下次運行時間" msgid "PipelineSchedules|None" -msgstr "" +msgstr "無" msgid "PipelineSchedules|Provide a short description for this pipeline" -msgstr "" +msgstr "為此流水線提供簡短描述" msgid "PipelineSchedules|Take ownership" -msgstr "" +msgstr "取得所有者" msgid "PipelineSchedules|Target" -msgstr "" +msgstr "目標" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "自定義" + +msgid "Pipeline|with stage" +msgstr "於階段" + +msgid "Pipeline|with stages" +msgstr "於階段" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "項目 '%{project_name}' 已進入刪除隊列。" + +msgid "Project '%{project_name}' was successfully created." +msgstr "項目 '%{project_name}' 已創建成功。" + +msgid "Project '%{project_name}' was successfully updated." +msgstr "項目 '%{project_name}' 已更新完成。" + +msgid "Project '%{project_name}' will be deleted." +msgstr "項目 '%{project_name}' 將被刪除。" + +msgid "Project access must be granted explicitly to each user." +msgstr "項目訪問權限必須明確授權給每個用戶。" + +msgid "Project export could not be deleted." +msgstr "無法刪除項目導出。" + +msgid "Project export has been deleted." +msgstr "項目導出已被刪除。" + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "項目導出鏈接已過期。請從項目設置中重新生成項目導出。" + +msgid "Project export started. A download link will be sent by email." +msgstr "項目導出已開始。下載鏈接將通過電子郵件發送。" + +msgid "Project home" +msgstr "項目首頁" + +msgid "ProjectFeature|Disabled" +msgstr "停用" + +msgid "ProjectFeature|Everyone with access" +msgstr "任何人都可訪問" + +msgid "ProjectFeature|Only team members" +msgstr "只限團隊成員" + +msgid "ProjectFileTree|Name" +msgstr "名稱" + +msgid "ProjectLastActivity|Never" +msgstr "從未" msgid "ProjectLifecycle|Stage" -msgstr "項目生命週期" +msgstr "階段" + +msgid "ProjectNetworkGraph|Graph" +msgstr "分支圖" msgid "Read more" msgstr "了解更多" +msgid "Readme" +msgstr "自述文件" + +msgid "RefSwitcher|Branches" +msgstr "分支" + +msgid "RefSwitcher|Tags" +msgstr "標籤" + msgid "Related Commits" msgstr "相關的提交" @@ -194,59 +655,164 @@ msgid "Related Merge Requests" msgstr "相關的合併請求" msgid "Related Merged Requests" -msgstr "相關已合併的合並請求" +msgstr "相關已合併的合併請求" + +msgid "Remind later" +msgstr "稍後提醒" + +msgid "Remove project" +msgstr "刪除項目" + +msgid "Request Access" +msgstr "申請權限" + +msgid "Revert this commit" +msgstr "還原此提交" + +msgid "Revert this merge request" +msgstr "還原此合併請求" msgid "Save pipeline schedule" -msgstr "" +msgstr "保存流水線計劃" msgid "Schedule a new pipeline" -msgstr "" +msgstr "新建流水線計劃" + +msgid "Scheduling Pipelines" +msgstr "流水線計劃" + +msgid "Search branches and tags" +msgstr "搜索分支和標籤" + +msgid "Select Archive Format" +msgstr "選擇下載格式" msgid "Select a timezone" -msgstr "" +msgstr "選擇時區" msgid "Select target branch" -msgstr "" +msgstr "選擇目標分支" + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "為賬號添加壹個用於推送或拉取的 %{protocol} 密碼。" + +msgid "Set up CI" +msgstr "設置 CI" + +msgid "Set up Koding" +msgstr "設置 Koding" + +msgid "Set up auto deploy" +msgstr "設置自動部署" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "設置密碼" msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "顯示 %d 個事件" +msgid "Source code" +msgstr "源代碼" + +msgid "StarProject|Star" +msgstr "星標" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "由此更改 %{new_merge_request}" + +msgid "Switch branch/tag" +msgstr "切換分支/標籤" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "標籤" + +msgid "Tags" +msgstr "標籤" + msgid "Target Branch" -msgstr "" +msgstr "目標分支" -msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." -msgstr "編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。" +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "編碼階段概述了從第壹次提交到創建合併請求的時間。創建第壹個合併請求後,數據將自動添加到此處。" msgid "The collection of events added to the data gathered for that stage." msgstr "與該階段相關的事件。" -msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." -msgstr "議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。" +msgid "The fork relationship has been removed." +msgstr "派生關係已被刪除。" + +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "議題階段概述了從創建議題到將議題添加到裏程碑或議題看板所花費的時間。創建第壹個議題後,數據將自動添加到此處.。" msgid "The phase of the development lifecycle." msgstr "項目生命週期中的各個階段。" -msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." -msgstr "計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。" +msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "流水線計劃會週期性重複運行指定分支或標籤的流水線。這些流水線將根據其關聯用戶繼承有限的項目訪問權限。" -msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "計劃階段概述了從議題添加到日程到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。" + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." msgstr "生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。" -msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." -msgstr "評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。" +msgid "The project can be accessed by any logged in user." +msgstr "該項目允許已登錄的用戶訪問。" + +msgid "The project can be accessed without any authentication." +msgstr "該項目允許任何人訪問。" -msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." -msgstr "預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。" +msgid "The repository for this project does not exist." +msgstr "此項目的存儲庫不存在。" -msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." -msgstr "測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。" +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "評審階段概述了從創建合併請求到合併的時間。當創建第壹個合併請求後,數據將自動添加到此處。" + +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "預發布階段概述了合併請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。" + +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "測試階段概述了 GitLab CI 為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。" msgid "The time taken by each data entry gathered by that stage." msgstr "該階段每條數據所花的時間" -msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." -msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。" +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." +msgstr "中位數是壹個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。" + +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "在創建壹個空的存儲庫或導入現有存儲庫之前,您將無法推送代碼。" msgid "Time before an issue gets scheduled" msgstr "議題被列入日程表的時間" @@ -255,11 +821,134 @@ msgid "Time before an issue starts implementation" msgstr "開始進行編碼前的時間" msgid "Time between merge request creation and merge/close" -msgstr "從創建合併請求到被合並或關閉的時間" +msgstr "從創建合併請求到被合併或關閉的時間" msgid "Time until first merge request" msgstr "創建第壹個合併請求之前的時間" +msgid "Timeago|%s days ago" +msgstr " %s 天前" + +msgid "Timeago|%s days remaining" +msgstr "剩餘 %s 天" + +msgid "Timeago|%s hours remaining" +msgstr "剩餘 %s 小時" + +msgid "Timeago|%s minutes ago" +msgstr " %s 分鐘前" + +msgid "Timeago|%s minutes remaining" +msgstr "剩餘 %s 分鐘" + +msgid "Timeago|%s months ago" +msgstr " %s 個月前" + +msgid "Timeago|%s months remaining" +msgstr "剩餘 %s 月" + +msgid "Timeago|%s seconds remaining" +msgstr "剩餘 %s 秒" + +msgid "Timeago|%s weeks ago" +msgstr " %s 星期前" + +msgid "Timeago|%s weeks remaining" +msgstr "剩餘 %s 星期" + +msgid "Timeago|%s years ago" +msgstr " %s 年前" + +msgid "Timeago|%s years remaining" +msgstr "剩餘 %s 年" + +msgid "Timeago|1 day remaining" +msgstr "剩餘 1 天" + +msgid "Timeago|1 hour remaining" +msgstr "剩餘 1 小時" + +msgid "Timeago|1 minute remaining" +msgstr "剩餘 1 分鐘" + +msgid "Timeago|1 month remaining" +msgstr "剩餘 1 個月" + +msgid "Timeago|1 week remaining" +msgstr "剩餘 1 星期" + +msgid "Timeago|1 year remaining" +msgstr "剩餘 1 年" + +msgid "Timeago|Past due" +msgstr "逾期" + +msgid "Timeago|a day ago" +msgstr " 1 天前" + +msgid "Timeago|a month ago" +msgstr " 1 個月前" + +msgid "Timeago|a week ago" +msgstr " 1 星期前" + +msgid "Timeago|a while" +msgstr " 剛剛" + +msgid "Timeago|a year ago" +msgstr " 1 年前" + +msgid "Timeago|about %s hours ago" +msgstr "約 %s 小時前" + +msgid "Timeago|about a minute ago" +msgstr "約 1 分鐘前" + +msgid "Timeago|about an hour ago" +msgstr "約 1 小時前" + +msgid "Timeago|in %s days" +msgstr " %s 天後" + +msgid "Timeago|in %s hours" +msgstr " %s 小時後" + +msgid "Timeago|in %s minutes" +msgstr " %s 分鐘後" + +msgid "Timeago|in %s months" +msgstr " %s 個月後" + +msgid "Timeago|in %s seconds" +msgstr " %s 秒後" + +msgid "Timeago|in %s weeks" +msgstr " %s 星期後" + +msgid "Timeago|in %s years" +msgstr " %s 年後" + +msgid "Timeago|in 1 day" +msgstr " 1 天後" + +msgid "Timeago|in 1 hour" +msgstr " 1 小時後" + +msgid "Timeago|in 1 minute" +msgstr " 1 分鐘後" + +msgid "Timeago|in 1 month" +msgstr " 1 月後" + +msgid "Timeago|in 1 week" +msgstr " 1 星期後" + +msgid "Timeago|in 1 year" +msgstr " 1 年後" + +msgid "Timeago|less than a minute ago" +msgstr "不到 1 分鐘前" + msgid "Time|hr" msgid_plural "Time|hrs" msgstr[0] "小時" @@ -277,15 +966,108 @@ msgstr "總時間" msgid "Total test time for all commits/merges" msgstr "所有提交和合併的總測試時間" +msgid "Unstar" +msgstr "取消星標" + +msgid "Upload New File" +msgstr "上傳新文件" + +msgid "Upload file" +msgstr "上傳文件" + +msgid "Use your global notification setting" +msgstr "使用全局通知設置" + +msgid "VisibilityLevel|Internal" +msgstr "內部" + +msgid "VisibilityLevel|Private" +msgstr "私有" + +msgid "VisibilityLevel|Public" +msgstr "公開" + msgid "Want to see the data? Please ask an administrator for access." msgstr "權限不足。如需查看相關數據,請向管理員申請權限。" msgid "We don't have enough data to show this stage." msgstr "該階段的數據不足,無法顯示。" +msgid "Withdraw Access Request" +msgstr "取消權限申请" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "即將要刪除 %{project_name_with_namespace}。\n" +"已刪除的項目無法恢複!\n" +"確定繼續嗎?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "即將刪除與源項目 %{forked_from_project} 的派生關系。確定繼續嗎?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "即將 %{project_name_with_namespace} 轉義給另壹個所有者。確定繼續嗎?" + +msgid "You can only add files when you are on a branch" +msgstr "只能在分支上添加文件" + +msgid "You have reached your project limit" +msgstr "您已達到項目數量限制" + +msgid "You must sign in to star a project" +msgstr "必須登錄才能對項目加星標" + msgid "You need permission." -msgstr "您需要相關的權限。" +msgstr "需要相關的權限。" + +msgid "You will not get any notifications via email" +msgstr "不會收到任何通知郵件" + +msgid "You will only receive notifications for the events you choose" +msgstr "只接收您選擇的事件通知" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "只接收您參與的主題的通知" + +msgid "You will receive notifications for any activity" +msgstr "接收所有活動的通知" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "只接收評論中提及(@)您的通知" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "在賬號上 %{set_password_link} 之前將無法通過 %{protocol} 拉取或推送代碼。" + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "在賬號中 %{add_ssh_key_link} 之前將無法通過 SSH 拉取或推送代碼。" + +msgid "Your name" +msgstr "您的名字" msgid "day" msgid_plural "days" msgstr[0] "天" + +msgid "new merge request" +msgstr "新建合併請求" + +msgid "notification emails" +msgstr "通知郵件" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "父級" + diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index e40723a9d8d..5130572d7ed 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -283,6 +283,9 @@ msgstr "權限不足。如需查看相關資料,請向管理員申請權限。 msgid "We don't have enough data to show this stage." msgstr "因該階段的資料不足而無法顯示相關資訊" +msgid "You have reached your project limit" +msgstr "" + msgid "You need permission." msgstr "您需要相關的權限。" diff --git a/package.json b/package.json index 29165fd4182..045f07ee2f9 100644 --- a/package.json +++ b/package.json @@ -72,13 +72,13 @@ "eslint-plugin-jasmine": "^2.1.0", "eslint-plugin-promise": "^3.5.0", "istanbul": "^0.4.5", - "jasmine-core": "^2.5.2", + "jasmine-core": "^2.6.3", "jasmine-jquery": "^2.1.1", - "karma": "^1.4.1", + "karma": "^1.7.0", + "karma-chrome-launcher": "^2.1.1", "karma-coverage-istanbul-reporter": "^0.2.0", "karma-jasmine": "^1.1.0", "karma-mocha-reporter": "^2.2.2", - "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.2", "nodemon": "^1.11.0", diff --git a/qa/Dockerfile b/qa/Dockerfile index 9e2a74ef991..f3a81a7e355 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,5 +1,6 @@ FROM ruby:2.3 LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" +ENV DEBIAN_FRONTEND noninteractive ## # Update APT sources and install some dependencies @@ -8,25 +9,21 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list RUN apt-get update && apt-get install -y wget git unzip xvfb ## -# At this point Google Chrome Beta is 59 - first version with headless support +# Install Google Chrome version with headless support # -RUN wget -q https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb -RUN dpkg -i google-chrome-beta_current_amd64.deb; apt-get -fy install +RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add - +RUN echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list +RUN apt-get update -q && apt-get install -y google-chrome-stable && apt-get clean ## # Install chromedriver to make it work with Selenium # -RUN wget -q https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip +RUN wget -q https://chromedriver.storage.googleapis.com/$(wget -q -O - https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip RUN unzip chromedriver_linux64.zip -d /usr/local/bin -RUN apt-get clean - WORKDIR /home/qa - COPY ./Gemfile* ./ - RUN bundle install - COPY ./ ./ ENTRYPOINT ["bin/test"] diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index 78a93828d36..b341aa3094a 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -55,7 +55,7 @@ module QA Capybara.register_driver :chrome do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { - 'binary' => '/opt/google/chrome-beta/google-chrome-beta', + 'binary' => '/usr/bin/google-chrome-stable', 'args' => %w[headless no-sandbox disable-gpu] } ) diff --git a/rubocop/cop/rspec/single_line_hook.rb b/rubocop/cop/rspec/single_line_hook.rb new file mode 100644 index 00000000000..be611054323 --- /dev/null +++ b/rubocop/cop/rspec/single_line_hook.rb @@ -0,0 +1,38 @@ +require 'rubocop-rspec' + +module RuboCop + module Cop + module RSpec + # This cop checks for single-line hook blocks + # + # @example + # + # # bad + # before { do_something } + # after(:each) { undo_something } + # + # # good + # before do + # do_something + # end + # + # after(:each) do + # undo_something + # end + class SingleLineHook < Cop + MESSAGE = "Don't use single-line hook blocks.".freeze + + def_node_search :rspec_hook?, <<~PATTERN + (send nil {:after :around :before} ...) + PATTERN + + def on_block(node) + return unless rspec_hook?(node) + return unless node.single_line? + + add_offense(node, :expression, MESSAGE) + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 6b8d127dde6..55d7708fa8c 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -15,3 +15,4 @@ require_relative 'cop/migration/remove_index' require_relative 'cop/migration/reversible_add_column_with_default' require_relative 'cop/migration/timestamps' require_relative 'cop/migration/update_column_in_batches' +require_relative 'cop/rspec/single_line_hook' diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb index c131d22a30a..a29853bf8df 100644 --- a/spec/controllers/admin/identities_controller_spec.rb +++ b/spec/controllers/admin/identities_controller_spec.rb @@ -2,7 +2,10 @@ require 'spec_helper' describe Admin::IdentitiesController do let(:admin) { create(:admin) } - before { sign_in(admin) } + + before do + sign_in(admin) + end describe 'UPDATE identity' do let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb index c94616d8508..4ca0cfc74e9 100644 --- a/spec/controllers/admin/services_controller_spec.rb +++ b/spec/controllers/admin/services_controller_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' describe Admin::ServicesController do let(:admin) { create(:admin) } - before { sign_in(admin) } + before do + sign_in(admin) + end describe 'GET #edit' do let!(:project) { create(:empty_project) } diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 7d6c317482f..69928a906c6 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -116,8 +116,8 @@ describe Admin::UsersController do it 'displays an alert' do go - expect(flash[:notice]). - to eq 'Two-factor Authentication has been disabled for this user' + expect(flash[:notice]) + .to eq 'Two-factor Authentication has been disabled for this user' end def go diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 3f99e2ff596..a2720c9b81e 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -99,6 +99,36 @@ describe ApplicationController do end end + describe 'response format' do + controller(described_class) do + def index + respond_to do |format| + format.json do + head :ok + end + end + end + end + + context 'when format is handled' do + let(:requested_format) { :json } + + it 'returns 200 response' do + get :index, private_token: user.private_token, format: requested_format + + expect(response).to have_http_status 200 + end + end + + context 'when format is not handled' do + it 'returns 404 response' do + get :index, private_token: user.private_token + + expect(response).to have_http_status 404 + end + end + end + describe '#authenticate_user_from_rss_token' do describe "authenticating a user from an RSS token" do controller(described_class) do diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 4c3a5ec49ef..b40f647644d 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -200,7 +200,9 @@ describe AutocompleteController do end context 'skip_users parameter included' do - before { sign_in(user) } + before do + sign_in(user) + end it 'skips the user IDs passed' do get(:users, skip_users: [user, user2].map(&:id)) diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index ed4ad7b600e..cce53f6697c 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -16,10 +16,14 @@ describe Groups::GroupMembersController do describe 'POST create' do let(:group_user) { create(:user) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when user does not have enough rights' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'returns 403' do post :create, group_id: group, @@ -32,7 +36,9 @@ describe Groups::GroupMembersController do end context 'when user has enough rights' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it 'adds user to members' do post :create, group_id: group, @@ -59,7 +65,9 @@ describe Groups::GroupMembersController do describe 'DELETE destroy' do let(:member) { create(:group_member, :developer, group: group) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 403' do @@ -71,7 +79,9 @@ describe Groups::GroupMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'returns 403' do delete :destroy, group_id: group, id: member @@ -82,7 +92,9 @@ describe Groups::GroupMembersController do end context 'when user has enough rights' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it '[HTML] removes user from members' do delete :destroy, group_id: group, id: member @@ -103,7 +115,9 @@ describe Groups::GroupMembersController do end describe 'DELETE leave' do - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -115,7 +129,9 @@ describe Groups::GroupMembersController do context 'when member is found' do context 'and is not an owner' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'removes user from members' do delete :leave, group_id: group @@ -134,7 +150,9 @@ describe Groups::GroupMembersController do end context 'and is an owner' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it 'cannot removes himself from the group' do delete :leave, group_id: group @@ -144,7 +162,9 @@ describe Groups::GroupMembersController do end context 'and is a requester' do - before { group.request_access(user) } + before do + group.request_access(user) + end it 'removes user from members' do delete :leave, group_id: group @@ -159,7 +179,9 @@ describe Groups::GroupMembersController do end describe 'POST request_access' do - before { sign_in(user) } + before do + sign_in(user) + end it 'creates a new GroupMember that is not a team member' do post :request_access, group_id: group @@ -174,7 +196,9 @@ describe Groups::GroupMembersController do describe 'POST approve_access_request' do let(:member) { create(:group_member, :access_request, group: group) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 403' do @@ -186,7 +210,9 @@ describe Groups::GroupMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'returns 403' do post :approve_access_request, group_id: group, id: member @@ -197,7 +223,9 @@ describe Groups::GroupMembersController do end context 'when user has enough rights' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it 'adds user to members' do post :approve_access_request, group_id: group, id: member diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index b0b24b1de1b..c4092303a67 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe GroupsController do let(:user) { create(:user) } - let(:group) { create(:group) } + let(:group) { create(:group, :public) } let(:project) { create(:empty_project, namespace: group) } let!(:group_member) { create(:group_member, group: group, user: user) } @@ -35,14 +35,15 @@ describe GroupsController do sign_in(user) end - it 'shows the public subgroups' do + it 'shows all subgroups' do get :subgroups, id: group.to_param - expect(assigns(:nested_groups)).to contain_exactly(public_subgroup) + expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup) end - context 'being member' do + context 'being member of private subgroup' do it 'shows public and private subgroups the user is member of' do + group_member.destroy! private_subgroup.add_guest(user) get :subgroups, id: group.to_param diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 0be7bc6a045..8ef10dabd4c 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -31,8 +31,8 @@ describe Import::BitbucketController do expires_at: expires_at, expires_in: expires_in, refresh_token: refresh_token) - allow_any_instance_of(OAuth2::Client). - to receive(:get_token).and_return(access_token) + allow_any_instance_of(OAuth2::Client) + .to receive(:get_token).and_return(access_token) stub_omniauth_provider('bitbucket') get :callback @@ -93,9 +93,9 @@ describe Import::BitbucketController do context "when the repository owner is the Bitbucket user" do context "when the Bitbucket user and GitLab user's usernames match" do it "takes the current user's namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -105,9 +105,9 @@ describe Import::BitbucketController do let(:bitbucket_username) { "someone_else" } it "takes the current user's namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -141,9 +141,9 @@ describe Import::BitbucketController do end it "takes the existing namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -151,8 +151,8 @@ describe Import::BitbucketController do context "when the namespace is not owned by the GitLab user" do it "doesn't create a project" do - expect(Gitlab::BitbucketImport::ProjectCreator). - not_to receive(:new) + expect(Gitlab::BitbucketImport::ProjectCreator) + .not_to receive(:new) post :create, format: :js end @@ -162,16 +162,16 @@ describe Import::BitbucketController do context "when a namespace with the Bitbucket user's username doesn't exist" do context "when current user can create namespaces" do it "creates the namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).and_return(double(execute: true)) expect { post :create, format: :js }.to change(Namespace, :count).by(1) end it "takes the new namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -183,16 +183,16 @@ describe Import::BitbucketController do end it "doesn't create the namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).and_return(double(execute: true)) expect { post :create, format: :js }.not_to change(Namespace, :count) end it "takes the current user's namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -210,9 +210,9 @@ describe Import::BitbucketController do end it 'takes the selected namespace and name' do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js } end @@ -222,26 +222,26 @@ describe Import::BitbucketController do let(:test_name) { 'test_name' } it 'takes the selected namespace and name' do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } end it 'creates the namespaces' do - allow(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + allow(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } } .to change { Namespace.count }.by(2) end it 'new namespace has the right parent' do - allow(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + allow(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } @@ -254,17 +254,17 @@ describe Import::BitbucketController do let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } it 'takes the selected namespace and name' do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } end it 'creates the namespaces' do - allow(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + allow(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } } .to change { Namespace.count }.by(2) diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 95696e14b6c..45c3fa075ef 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -21,10 +21,10 @@ describe Import::GithubController do describe "GET callback" do it "updates access token" do token = "asdasd12345" - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:get_token).and_return(token) - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:github_options).and_return({}) + allow_any_instance_of(Gitlab::GithubImport::Client) + .to receive(:get_token).and_return(token) + allow_any_instance_of(Gitlab::GithubImport::Client) + .to receive(:github_options).and_return({}) stub_omniauth_provider('github') get :callback diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 3afd09063d7..997107dadea 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -18,8 +18,8 @@ describe Import::GitlabController do describe "GET callback" do it "updates access token" do - allow_any_instance_of(Gitlab::GitlabImport::Client). - to receive(:get_token).and_return(token) + allow_any_instance_of(Gitlab::GitlabImport::Client) + .to receive(:get_token).and_return(token) stub_omniauth_provider('gitlab') get :callback @@ -78,9 +78,9 @@ describe Import::GitlabController do context "when the repository owner is the GitLab.com user" do context "when the GitLab.com user and GitLab server user's usernames match" do it "takes the current user's namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, user.namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -90,9 +90,9 @@ describe Import::GitlabController do let(:gitlab_username) { "someone_else" } it "takes the current user's namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, user.namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -116,9 +116,9 @@ describe Import::GitlabController do end it "takes the existing namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, existing_namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, existing_namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -126,8 +126,8 @@ describe Import::GitlabController do context "when the namespace is not owned by the GitLab server user" do it "doesn't create a project" do - expect(Gitlab::GitlabImport::ProjectCreator). - not_to receive(:new) + expect(Gitlab::GitlabImport::ProjectCreator) + .not_to receive(:new) post :create, format: :js end @@ -137,16 +137,16 @@ describe Import::GitlabController do context "when a namespace with the GitLab.com user's username doesn't exist" do context "when current user can create namespaces" do it "creates the namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).and_return(double(execute: true)) expect { post :create, format: :js }.to change(Namespace, :count).by(1) end it "takes the new namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -158,16 +158,16 @@ describe Import::GitlabController do end it "doesn't create the namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).and_return(double(execute: true)) expect { post :create, format: :js }.not_to change(Namespace, :count) end it "takes the current user's namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, user.namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) + .and_return(double(execute: true)) post :create, format: :js end @@ -183,9 +183,9 @@ describe Import::GitlabController do end it 'takes the selected namespace and name' do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, nested_namespace, user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, nested_namespace, user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: nested_namespace.full_path, format: :js } end @@ -195,26 +195,26 @@ describe Import::GitlabController do let(:test_name) { 'test_name' } it 'takes the selected namespace and name' do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/bar', format: :js } end it 'creates the namespaces' do - allow(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + allow(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) expect { post :create, { target_namespace: 'foo/bar', format: :js } } .to change { Namespace.count }.by(2) end it 'new namespace has the right parent' do - allow(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + allow(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/bar', format: :js } @@ -227,17 +227,17 @@ describe Import::GitlabController do let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } it 'takes the selected namespace and name' do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + expect(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/foobar/bar', format: :js } end it 'creates the namespaces' do - allow(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params). - and_return(double(execute: true)) + allow(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) + .and_return(double(execute: true)) expect { post :create, { target_namespace: 'foo/foobar/bar', format: :js } } .to change { Namespace.count }.by(2) diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb index 9e3a31e1a6b..6b690407ce3 100644 --- a/spec/controllers/notification_settings_controller_spec.rb +++ b/spec/controllers/notification_settings_controller_spec.rb @@ -58,7 +58,10 @@ describe NotificationSettingsController do expect(response.status).to eq 200 expect(notification_setting.level).to eq("custom") - expect(notification_setting.events).to eq(custom_events) + + custom_events.each do |event, value| + expect(notification_setting.event_enabled?(event)).to eq(value) + end end end end @@ -86,7 +89,10 @@ describe NotificationSettingsController do expect(response.status).to eq 200 expect(notification_setting.level).to eq("custom") - expect(notification_setting.events).to eq(custom_events) + + custom_events.each do |event, value| + expect(notification_setting.event_enabled?(event)).to eq(value) + end end end end @@ -94,7 +100,10 @@ describe NotificationSettingsController do context 'not authorized' do let(:private_project) { create(:empty_project, :private) } - before { sign_in(user) } + + before do + sign_in(user) + end it 'returns 404' do post :create, @@ -120,7 +129,9 @@ describe NotificationSettingsController do end context 'when authorized' do - before{ sign_in(user) } + before do + sign_in(user) + end it 'returns success' do put :update, @@ -152,7 +163,9 @@ describe NotificationSettingsController do context 'not authorized' do let(:other_user) { create(:user) } - before { sign_in(other_user) } + before do + sign_in(other_user) + end it 'returns 404' do put :update, diff --git a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb index 98a43e278b2..ed08a4c1bf2 100644 --- a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb +++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb @@ -4,7 +4,9 @@ describe Profiles::PersonalAccessTokensController do let(:user) { create(:user) } let(:token_attributes) { attributes_for(:personal_access_token) } - before { sign_in(user) } + before do + sign_in(user) + end describe '#create' do def created_token @@ -38,7 +40,9 @@ describe Profiles::PersonalAccessTokensController do let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) } let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) } - before { get :index } + before do + get :index + end it "retrieves active personal access tokens" do expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token) diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 7b3aa0491c7..a5f544b4f92 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -43,7 +43,8 @@ describe Profiles::PreferencesController do dashboard: 'stars' }.with_indifferent_access - expect(user).to receive(:update_attributes).with(prefs) + expect(user).to receive(:assign_attributes).with(prefs) + expect(user).to receive(:save) go params: prefs end @@ -51,7 +52,7 @@ describe Profiles::PreferencesController do context 'on failed update' do it 'sets the flash' do - expect(user).to receive(:update_attributes).and_return(false) + expect(user).to receive(:save).and_return(false) go diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 3b3caa9d3e6..c20cf6a4291 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -47,8 +47,8 @@ describe Projects::BlobController do context 'redirect to tree' do let(:id) { 'markdown/doc' } it 'redirects' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") + expect(subject) + .to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") end end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index f9e21f9d8f6..14426b09c73 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -32,8 +32,8 @@ describe Projects::BranchesController do let(:branch) { "merge_branch" } let(:ref) { "master" } it 'redirects' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") + expect(subject) + .to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") end end @@ -41,8 +41,8 @@ describe Projects::BranchesController do let(:branch) { "<script>alert('merge');</script>" } let(:ref) { "master" } it 'redirects' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") + expect(subject) + .to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") end end @@ -81,8 +81,8 @@ describe Projects::BranchesController do branch_name: branch, issue_iid: issue.iid - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/1-feature-branch") + expect(subject) + .to redirect_to("/#{project.path_with_namespace}/tree/1-feature-branch") end it 'posts a system note' do diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 69e4706dc71..e10da40eaab 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -66,8 +66,8 @@ describe Projects::CommitController do end it "does not escape Html" do - allow_any_instance_of(Commit).to receive(:"to_#{format}"). - and_return('HTML entities &<>" ') + allow_any_instance_of(Commit).to receive(:"to_#{format}") + .and_return('HTML entities &<>" ') go(id: commit.id, format: format) @@ -281,7 +281,9 @@ describe Projects::CommitController do end context 'when the path does not exist in the diff' do - before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) } + before do + diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -302,7 +304,9 @@ describe Projects::CommitController do end context 'when the commit does not exist' do - before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 15ac4e0925a..8f4694c9854 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -128,7 +128,9 @@ describe Projects::CompareController do end context 'when the path does not exist in the diff' do - before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) } + before do + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -149,7 +151,9 @@ describe Projects::CompareController do end context 'when the from ref does not exist' do - before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -157,7 +161,9 @@ describe Projects::CompareController do end context 'when the to ref does not exist' do - before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb index 4c69443314d..0dbfcf97f6f 100644 --- a/spec/controllers/projects/deployments_controller_spec.rb +++ b/spec/controllers/projects/deployments_controller_spec.rb @@ -42,6 +42,7 @@ describe Projects::DeploymentsController do before do allow(controller).to receive(:deployment).and_return(deployment) end + context 'when metrics are disabled' do before do allow(deployment).to receive(:has_metrics?).and_return false @@ -108,6 +109,69 @@ describe Projects::DeploymentsController do end end + describe 'GET #additional_metrics' do + let(:deployment) { create(:deployment, project: project, environment: environment) } + + before do + allow(controller).to receive(:deployment).and_return(deployment) + end + + context 'when metrics are disabled' do + before do + allow(deployment).to receive(:has_metrics?).and_return false + end + + it 'responds with not found' do + get :metrics, deployment_params(id: deployment.id) + + expect(response).to be_not_found + end + end + + context 'when metrics are enabled' do + let(:prometheus_service) { double('prometheus_service') } + + before do + allow(deployment.project).to receive(:prometheus_service).and_return(prometheus_service) + end + + context 'when environment has no metrics' do + before do + expect(deployment).to receive(:additional_metrics).and_return({}) + end + + it 'returns a empty response 204 response' do + get :additional_metrics, deployment_params(id: deployment.id, format: :json) + expect(response).to have_http_status(204) + expect(response.body).to eq('') + end + end + + context 'when environment has some metrics' do + let(:empty_metrics) do + { + success: true, + metrics: {}, + last_update: 42 + } + end + + before do + expect(deployment).to receive(:additional_metrics).and_return(empty_metrics) + end + + it 'returns a metrics JSON document' do + get :additional_metrics, deployment_params(id: deployment.id, format: :json) + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['metrics']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end + end + end + def deployment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index f6840578145..ad0b046742d 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -58,9 +58,11 @@ describe Projects::EnvironmentsController do expect(json_response['stopped_count']).to eq 1 end - it 'sets the polling interval header' do + it 'does not set the polling interval header' do + # TODO, this is a temporary fix, see follow up issue: + # https://gitlab.com/gitlab-org/gitlab-ee/issues/2677 expect(response).to have_http_status(:ok) - expect(response.headers['Poll-Interval']).to eq("3000") + expect(response.headers['Poll-Interval']).to be_nil end end @@ -233,14 +235,14 @@ describe Projects::EnvironmentsController do context 'and valid id' do it 'returns the first terminal for the environment' do - expect_any_instance_of(Environment). - to receive(:terminals). - and_return([:fake_terminal]) + expect_any_instance_of(Environment) + .to receive(:terminals) + .and_return([:fake_terminal]) - expect(Gitlab::Workhorse). - to receive(:terminal_websocket). - with(:fake_terminal). - and_return(workhorse: :response) + expect(Gitlab::Workhorse) + .to receive(:terminal_websocket) + .with(:fake_terminal) + .and_return(workhorse: :response) get :terminal_websocket_authorize, environment_params @@ -316,6 +318,48 @@ describe Projects::EnvironmentsController do end end + describe 'GET #additional_metrics' do + before do + allow(controller).to receive(:environment).and_return(environment) + end + + context 'when environment has no metrics' do + before do + expect(environment).to receive(:additional_metrics).and_return(nil) + end + + context 'when requesting metrics as JSON' do + it 'returns a metrics JSON document' do + get :additional_metrics, environment_params(format: :json) + + expect(response).to have_http_status(204) + expect(json_response).to eq({}) + end + end + end + + context 'when environment has some metrics' do + before do + expect(environment) + .to receive(:additional_metrics) + .and_return({ + success: true, + data: {}, + last_update: 42 + }) + end + + it 'returns a metrics JSON document' do + get :additional_metrics, environment_params(format: :json) + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['data']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end + end + def environment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index 8282d79298f..dc8290c438e 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -14,7 +14,9 @@ describe Projects::ForksController do end context 'when fork is public' do - before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) } + before do + forked_project.update_attribute(:visibility_level, Project::PUBLIC) + end it 'is visible for non logged in users' do get_forks @@ -35,7 +37,9 @@ describe Projects::ForksController do end context 'when user is logged in' do - before { sign_in(project.creator) } + before do + sign_in(project.creator) + end context 'when user is not a Project member neither a group member' do it 'does not see the Project listed' do @@ -46,7 +50,9 @@ describe Projects::ForksController do end context 'when user is a member of the Project' do - before { forked_project.team << [project.creator, :developer] } + before do + forked_project.team << [project.creator, :developer] + end it 'sees the project listed' do get_forks @@ -56,7 +62,9 @@ describe Projects::ForksController do end context 'when user is a member of the Group' do - before { forked_project.group.add_developer(project.creator) } + before do + forked_project.group.add_developer(project.creator) + end it 'sees the project listed' do get_forks diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index ca4a8e871c0..b5435357f53 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -22,7 +22,10 @@ describe Projects::GroupLinksController do end context 'when user has access to group he want to link project to' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end + include_context 'link project to group' it 'links project with selected group' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index b65e9e0dfc0..9f98427a86b 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -212,7 +212,9 @@ describe Projects::IssuesController do let(:another_project) { create(:empty_project, :private) } context 'when user has access to move issue' do - before { another_project.team << [user, :reporter] } + before do + another_project.team << [user, :reporter] + end it 'moves issue to another project' do move_issue @@ -250,14 +252,18 @@ describe Projects::IssuesController do end context 'when an issue is identified as spam' do - before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) } + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end context 'when captcha is not verified' do def update_spam_issue update_issue(title: 'Spam Title', description: 'Spam lives here') end - before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + end it 'rejects an issue recognized as a spam' do expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) @@ -322,8 +328,8 @@ describe Projects::IssuesController do it 'redirect to issue page' do update_verified_issue - expect(response). - to redirect_to(namespace_project_issue_path(project.namespace, project, issue)) + expect(response) + .to redirect_to(namespace_project_issue_path(project.namespace, project, issue)) end it 'accepts an issue after recaptcha is verified' do @@ -337,8 +343,8 @@ describe Projects::IssuesController do it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do spam_log = create(:spam_log) - expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }. - not_to change { SpamLog.last.recaptcha_verified } + expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) } + .not_to change { SpamLog.last.recaptcha_verified } end end end @@ -620,14 +626,18 @@ describe Projects::IssuesController do end context 'when an issue is identified as spam' do - before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) } + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end context 'when captcha is not verified' do def post_spam_issue post_new_issue(title: 'Spam Title', description: 'Spam lives here') end - before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + end it 'rejects an issue recognized as a spam' do expect { post_spam_issue }.not_to change(Issue, :count) @@ -675,8 +685,8 @@ describe Projects::IssuesController do it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do spam_log = create(:spam_log) - expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }. - not_to change { SpamLog.last.recaptcha_verified } + expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) } + .not_to change { SpamLog.last.recaptcha_verified } end end end @@ -692,7 +702,7 @@ describe Projects::IssuesController do end end - context 'when description has slash commands' do + context 'when description has quick actions' do before do sign_in(user) end @@ -739,7 +749,10 @@ describe Projects::IssuesController do describe "DELETE #destroy" do context "when the user is a developer" do - before { sign_in(user) } + before do + sign_in(user) + end + it "rejects a developer to destroy an issue" do delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid expect(response).to have_http_status(404) @@ -751,7 +764,9 @@ describe Projects::IssuesController do let(:namespace) { create(:namespace, owner: owner) } let(:project) { create(:empty_project, namespace: namespace) } - before { sign_in(owner) } + before do + sign_in(owner) + end it "deletes the issue" do delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 4a737587899..472e5fc51a0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -28,7 +28,7 @@ describe Projects::JobsController do get_index(scope: 'running') end - it 'has only running builds' do + it 'has only running jobs' do expect(response).to have_http_status(:ok) expect(assigns(:builds).first.status).to eq('running') end @@ -41,7 +41,7 @@ describe Projects::JobsController do get_index(scope: 'finished') end - it 'has only finished builds' do + it 'has only finished jobs' do expect(response).to have_http_status(:ok) expect(assigns(:builds).first.status).to eq('success') end @@ -67,7 +67,7 @@ describe Projects::JobsController do context 'number of queries' do before do Ci::Build::AVAILABLE_STATUSES.each do |status| - create_build(status, status) + create_job(status, status) end end @@ -76,7 +76,7 @@ describe Projects::JobsController do expect(recorded.count).to be_within(5).of(7) end - def create_build(name, status) + def create_job(name, status) pipeline = create(:ci_pipeline, project: project) create(:ci_build, :tags, :triggered, :artifacts, pipeline: pipeline, name: name, status: status) @@ -94,21 +94,21 @@ describe Projects::JobsController do end describe 'GET show' do - let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } + let!(:job) { create(:ci_build, :failed, pipeline: pipeline) } context 'when requesting HTML' do - context 'when build exists' do + context 'when job exists' do before do - get_show(id: build.id) + get_show(id: job.id) end - it 'has a build' do + it 'has a job' do expect(response).to have_http_status(:ok) - expect(assigns(:build).id).to eq(build.id) + expect(assigns(:build).id).to eq(job.id) end end - context 'when build does not exist' do + context 'when job does not exist' do before do get_show(id: 1234) end @@ -128,12 +128,12 @@ describe Projects::JobsController do allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) - get_show(id: build.id, format: :json) + get_show(id: job.id, format: :json) end it 'exposes needed information' do expect(response).to have_http_status(:ok) - expect(json_response['raw_path']).to match(/builds\/\d+\/raw\z/) + expect(json_response['raw_path']).to match(/jobs\/\d+\/raw\z/) expect(json_response.dig('merge_request', 'path')).to match(/merge_requests\/\d+\z/) expect(json_response['new_issue_path']) .to include('/issues/new') @@ -155,35 +155,35 @@ describe Projects::JobsController do get_trace end - context 'when build has a trace' do - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + context 'when job has a trace' do + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } it 'returns a trace' do expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status + expect(json_response['id']).to eq job.id + expect(json_response['status']).to eq job.status expect(json_response['html']).to eq('BUILD TRACE') end end - context 'when build has no traces' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job has no traces' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'returns no traces' do expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status + expect(json_response['id']).to eq job.id + expect(json_response['status']).to eq job.status expect(json_response['html']).to be_nil end end - context 'when build has a trace with ANSI sequence and Unicode' do - let(:build) { create(:ci_build, :unicode_trace, pipeline: pipeline) } + context 'when job has a trace with ANSI sequence and Unicode' do + let(:job) { create(:ci_build, :unicode_trace, pipeline: pipeline) } it 'returns a trace with Unicode' do expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status + expect(json_response['id']).to eq job.id + expect(json_response['status']).to eq job.status expect(json_response['html']).to include("ヾ(´༎ຶД༎ຶ`)ノ") end end @@ -191,23 +191,23 @@ describe Projects::JobsController do def get_trace get :trace, namespace_id: project.namespace, project_id: project, - id: build.id, + id: job.id, format: :json end end describe 'GET status.json' do - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:status) { build.detailed_status(double('user')) } + let(:job) { create(:ci_build, pipeline: pipeline) } + let(:status) { job.detailed_status(double('user')) } before do get :status, namespace_id: project.namespace, project_id: project, - id: build.id, + id: job.id, format: :json end - it 'return a detailed build status in json' do + it 'return a detailed job status in json' do expect(response).to have_http_status(:ok) expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label @@ -224,17 +224,17 @@ describe Projects::JobsController do post_retry end - context 'when build is retryable' do - let(:build) { create(:ci_build, :retryable, pipeline: pipeline) } + context 'when job is retryable' do + let(:job) { create(:ci_build, :retryable, pipeline: pipeline) } - it 'redirects to the retried build page' do + it 'redirects to the retried job page' do expect(response).to have_http_status(:found) expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id)) end end - context 'when build is not retryable' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job is not retryable' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'renders unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -244,7 +244,7 @@ describe Projects::JobsController do def post_retry post :retry, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -260,21 +260,21 @@ describe Projects::JobsController do post_play end - context 'when build is playable' do - let(:build) { create(:ci_build, :playable, pipeline: pipeline) } + context 'when job is playable' do + let(:job) { create(:ci_build, :playable, pipeline: pipeline) } - it 'redirects to the played build page' do + it 'redirects to the played job page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_job_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: job.id)) end it 'transits to pending' do - expect(build.reload).to be_pending + expect(job.reload).to be_pending end end - context 'when build is not playable' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job is not playable' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'renders unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -284,7 +284,7 @@ describe Projects::JobsController do def post_play post :play, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -296,21 +296,21 @@ describe Projects::JobsController do post_cancel end - context 'when build is cancelable' do - let(:build) { create(:ci_build, :cancelable, pipeline: pipeline) } + context 'when job is cancelable' do + let(:job) { create(:ci_build, :cancelable, pipeline: pipeline) } - it 'redirects to the canceled build page' do + it 'redirects to the canceled job page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_job_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: job.id)) end it 'transits to canceled' do - expect(build.reload).to be_canceled + expect(job.reload).to be_canceled end end - context 'when build is not cancelable' do - let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + context 'when job is not cancelable' do + let(:job) { create(:ci_build, :canceled, pipeline: pipeline) } it 'returns unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -320,7 +320,7 @@ describe Projects::JobsController do def post_cancel post :cancel, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -330,7 +330,7 @@ describe Projects::JobsController do sign_in(user) end - context 'when builds are cancelable' do + context 'when jobs are cancelable' do before do create_list(:ci_build, 2, :cancelable, pipeline: pipeline) @@ -347,7 +347,7 @@ describe Projects::JobsController do end end - context 'when builds are not cancelable' do + context 'when jobs are not cancelable' do before do create_list(:ci_build, 2, :canceled, pipeline: pipeline) @@ -374,26 +374,26 @@ describe Projects::JobsController do post_erase end - context 'when build is erasable' do - let(:build) { create(:ci_build, :erasable, :trace, pipeline: pipeline) } + context 'when job is erasable' do + let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline) } - it 'redirects to the erased build page' do + it 'redirects to the erased job page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_job_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: job.id)) end it 'erases artifacts' do - expect(build.artifacts_file.exists?).to be_falsey - expect(build.artifacts_metadata.exists?).to be_falsey + expect(job.artifacts_file.exists?).to be_falsey + expect(job.artifacts_metadata.exists?).to be_falsey end it 'erases trace' do - expect(build.trace.exist?).to be_falsey + expect(job.trace.exist?).to be_falsey end end - context 'when build is not erasable' do - let(:build) { create(:ci_build, :erased, pipeline: pipeline) } + context 'when job is not erasable' do + let(:job) { create(:ci_build, :erased, pipeline: pipeline) } it 'returns unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -403,7 +403,7 @@ describe Projects::JobsController do def post_erase post :erase, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -412,8 +412,8 @@ describe Projects::JobsController do get_raw end - context 'when build has a trace file' do - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + context 'when job has a trace file' do + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } it 'send a trace file' do expect(response).to have_http_status(:ok) @@ -422,8 +422,8 @@ describe Projects::JobsController do end end - context 'when build does not have a trace file' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job does not have a trace file' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'returns not_found' do expect(response).to have_http_status(:not_found) @@ -433,7 +433,7 @@ describe Projects::JobsController do def get_raw post :raw, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end end diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb index c5abf11cfa5..422a8b6fac0 100644 --- a/spec/controllers/projects/mattermosts_controller_spec.rb +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -11,8 +11,8 @@ describe Projects::MattermostsController do describe 'GET #new' do before do - allow_any_instance_of(MattermostSlashCommandsService). - to receive(:list_teams).and_return([]) + allow_any_instance_of(MattermostSlashCommandsService) + .to receive(:list_teams).and_return([]) end it 'accepts the request' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6e1c91738db..6817c2652fd 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -19,7 +19,10 @@ describe Projects::MergeRequestsController do render_views let(:fork_project) { create(:forked_project_with_submodules) } - before { fork_project.team << [user, :master] } + + before do + fork_project.team << [user, :master] + end context 'when rendering HTML response' do it 'renders new merge request widget template' do @@ -328,7 +331,9 @@ describe Projects::MergeRequestsController do end context 'when the sha parameter does not match the source SHA' do - before { post :merge, base_params.merge(sha: 'foo') } + before do + post :merge, base_params.merge(sha: 'foo') + end it 'returns :sha_mismatch' do expect(json_response).to eq('status' => 'sha_mismatch') @@ -473,7 +478,9 @@ describe Projects::MergeRequestsController do let(:namespace) { create(:namespace, owner: owner) } let(:project) { create(:project, namespace: namespace) } - before { sign_in owner } + before do + sign_in owner + end it "deletes the merge request" do delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid @@ -505,7 +512,9 @@ describe Projects::MergeRequestsController do context 'with default params' do context 'as html' do - before { go(format: 'html') } + before do + go(format: 'html') + end it 'renders the diff template' do expect(response).to render_template('diffs') @@ -513,7 +522,9 @@ describe Projects::MergeRequestsController do end context 'as json' do - before { go(format: 'json') } + before do + go(format: 'json') + end it 'renders the diffs template to a string' do expect(response).to render_template('projects/merge_requests/show/_diffs') @@ -544,7 +555,9 @@ describe Projects::MergeRequestsController do context 'with ignore_whitespace_change' do context 'as html' do - before { go(format: 'html', w: 1) } + before do + go(format: 'html', w: 1) + end it 'renders the diff template' do expect(response).to render_template('diffs') @@ -552,7 +565,9 @@ describe Projects::MergeRequestsController do end context 'as json' do - before { go(format: 'json', w: 1) } + before do + go(format: 'json', w: 1) + end it 'renders the diffs template to a string' do expect(response).to render_template('projects/merge_requests/show/_diffs') @@ -562,7 +577,9 @@ describe Projects::MergeRequestsController do end context 'with view' do - before { go(view: 'parallel') } + before do + go(view: 'parallel') + end it 'saves the preferred diff view in a cookie' do expect(response.cookies['diff_view']).to eq('parallel') @@ -605,7 +622,9 @@ describe Projects::MergeRequestsController do end context 'when the path does not exist in the diff' do - before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') } + before do + diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -626,7 +645,9 @@ describe Projects::MergeRequestsController do end context 'when the merge request does not exist' do - before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -670,7 +691,9 @@ describe Projects::MergeRequestsController do context 'when the source branch is in a different project to the target' do let(:other_project) { create(:project) } - before { other_project.team << [user, :master] } + before do + other_project.team << [user, :master] + end context 'when the path exists in the diff' do it 'disables diff notes' do @@ -690,7 +713,9 @@ describe Projects::MergeRequestsController do end context 'when the path does not exist in the diff' do - before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) } + before do + diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -758,8 +783,8 @@ describe Projects::MergeRequestsController do describe 'GET conflicts' do context 'when the conflicts cannot be resolved in the UI' do before do - allow_any_instance_of(Gitlab::Conflict::Parser). - to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) + allow_any_instance_of(Gitlab::Conflict::Parser) + .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) get :conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, @@ -901,8 +926,8 @@ describe Projects::MergeRequestsController do context 'when the conflicts cannot be resolved in the UI' do before do - allow_any_instance_of(Gitlab::Conflict::Parser). - to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) + allow_any_instance_of(Gitlab::Conflict::Parser) + .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) conflict_for_path('files/ruby/regex.rb') end @@ -913,7 +938,9 @@ describe Projects::MergeRequestsController do end context 'when the file does not exist cannot be resolved in the UI' do - before { conflict_for_path('files/ruby/regexp.rb') } + before do + conflict_for_path('files/ruby/regexp.rb') + end it 'returns a 404 status code' do expect(response).to have_http_status(:not_found) @@ -923,16 +950,18 @@ describe Projects::MergeRequestsController do context 'with an existing file' do let(:path) { 'files/ruby/regex.rb' } - before { conflict_for_path(path) } + before do + conflict_for_path(path) + end it 'returns a 200 status code' do expect(response).to have_http_status(:ok) end it 'returns the file in JSON format' do - content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts). - file_for_path(path, path). - content + content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts) + .file_for_path(path, path) + .content expect(json_response).to include('old_path' => path, 'new_path' => path, @@ -1056,9 +1085,9 @@ describe Projects::MergeRequestsController do context 'when a file has identical content to the conflict' do before do - content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts). - file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb'). - content + content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts) + .file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb') + .content resolved_files = [ { @@ -1124,9 +1153,9 @@ describe Projects::MergeRequestsController do end it 'calls MergeRequests::AssignIssuesService' do - expect(MergeRequests::AssignIssuesService).to receive(:new). - with(project, user, merge_request: merge_request). - and_return(double(execute: { count: 1 })) + expect(MergeRequests::AssignIssuesService).to receive(:new) + .with(project, user, merge_request: merge_request) + .and_return(double(execute: { count: 1 })) post_assign_issues end @@ -1195,7 +1224,9 @@ describe Projects::MergeRequestsController do end context 'when head_pipeline does not exist' do - before { get_pipeline_status } + before do + get_pipeline_status + end it 'return empty' do expect(response).to have_http_status(:ok) diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 954f89e3854..734532668d3 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -5,9 +5,12 @@ describe Projects::PipelinesController do let(:user) { create(:user) } let(:project) { create(:empty_project, :public) } + let(:feature) { ProjectFeature::DISABLED } before do project.add_developer(user) + project.project_feature.update( + builds_access_level: feature) sign_in(user) end @@ -153,16 +156,26 @@ describe Projects::PipelinesController do format: :json end - it 'retries a pipeline without returning any content' do - expect(response).to have_http_status(:no_content) - expect(build.reload).to be_retried + context 'when builds are enabled' do + let(:feature) { ProjectFeature::ENABLED } + + it 'retries a pipeline without returning any content' do + expect(response).to have_http_status(:no_content) + expect(build.reload).to be_retried + end + end + + context 'when builds are disabled' do + it 'fails to retry pipeline' do + expect(response).to have_http_status(:not_found) + end end end describe 'POST cancel.json' do let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:build) { create(:ci_build, :running, pipeline: pipeline) } - + before do post :cancel, namespace_id: project.namespace, project_id: project, @@ -170,9 +183,19 @@ describe Projects::PipelinesController do format: :json end - it 'cancels a pipeline without returning any content' do - expect(response).to have_http_status(:no_content) - expect(pipeline.reload).to be_canceled + context 'when builds are enabled' do + let(:feature) { ProjectFeature::ENABLED } + + it 'cancels a pipeline without returning any content' do + expect(response).to have_http_status(:no_content) + expect(pipeline.reload).to be_canceled + end + end + + context 'when builds are disabled' do + it 'fails to retry pipeline' do + expect(response).to have_http_status(:not_found) + end end end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 2294d5df581..f2b59ba82ca 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -16,10 +16,14 @@ describe Projects::ProjectMembersController do describe 'POST create' do let(:project_user) { create(:user) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'returns 404' do post :create, namespace_id: project.namespace, @@ -33,7 +37,9 @@ describe Projects::ProjectMembersController do end context 'when user has enough rights' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'adds user to members' do expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(status: :success) @@ -64,7 +70,9 @@ describe Projects::ProjectMembersController do describe 'DELETE destroy' do let(:member) { create(:project_member, :developer, project: project) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -78,7 +86,9 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'returns 404' do delete :destroy, namespace_id: project.namespace, @@ -91,7 +101,9 @@ describe Projects::ProjectMembersController do end context 'when user has enough rights' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it '[HTML] removes user from members' do delete :destroy, namespace_id: project.namespace, @@ -117,7 +129,9 @@ describe Projects::ProjectMembersController do end describe 'DELETE leave' do - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -130,7 +144,9 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'and is not an owner' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'removes user from members' do delete :leave, namespace_id: project.namespace, @@ -145,7 +161,9 @@ describe Projects::ProjectMembersController do context 'and is an owner' do let(:project) { create(:empty_project, namespace: user.namespace) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'cannot remove himself from the project' do delete :leave, namespace_id: project.namespace, @@ -156,7 +174,9 @@ describe Projects::ProjectMembersController do end context 'and is a requester' do - before { project.request_access(user) } + before do + project.request_access(user) + end it 'removes user from members' do delete :leave, namespace_id: project.namespace, @@ -172,7 +192,9 @@ describe Projects::ProjectMembersController do end describe 'POST request_access' do - before { sign_in(user) } + before do + sign_in(user) + end it 'creates a new ProjectMember that is not a team member' do post :request_access, namespace_id: project.namespace, @@ -190,7 +212,9 @@ describe Projects::ProjectMembersController do describe 'POST approve' do let(:member) { create(:project_member, :access_request, project: project) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -204,7 +228,9 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'returns 404' do post :approve_access_request, namespace_id: project.namespace, @@ -217,7 +243,9 @@ describe Projects::ProjectMembersController do end context 'when user has enough rights' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'adds user to members' do post :approve_access_request, namespace_id: project.namespace, @@ -252,7 +280,10 @@ describe Projects::ProjectMembersController do end context 'when user can access source project members' do - before { another_project.team << [user, :guest] } + before do + another_project.team << [user, :guest] + end + include_context 'import applied' it 'imports source project members' do diff --git a/spec/controllers/projects/prometheus_controller_spec.rb b/spec/controllers/projects/prometheus_controller_spec.rb new file mode 100644 index 00000000000..eddf7275975 --- /dev/null +++ b/spec/controllers/projects/prometheus_controller_spec.rb @@ -0,0 +1,59 @@ +require('spec_helper') + +describe Projects::PrometheusController do + let(:user) { create(:user) } + let!(:project) { create(:empty_project) } + + let(:prometheus_service) { double('prometheus_service') } + + before do + allow(controller).to receive(:project).and_return(project) + allow(project).to receive(:prometheus_service).and_return(prometheus_service) + + project.add_master(user) + sign_in(user) + end + + describe 'GET #active_metrics' do + context 'when prometheus metrics are enabled' do + context 'when data is not present' do + before do + allow(prometheus_service).to receive(:matched_metrics).and_return({}) + end + + it 'returns no content response' do + get :active_metrics, project_params(format: :json) + + expect(response).to have_http_status(204) + end + end + + context 'when data is available' do + let(:sample_response) { { some_data: 1 } } + + before do + allow(prometheus_service).to receive(:matched_metrics).and_return(sample_response) + end + + it 'returns no content response' do + get :active_metrics, project_params(format: :json) + + expect(response).to have_http_status(200) + expect(json_response).to eq(sample_response.deep_stringify_keys) + end + end + + context 'when requesting non json response' do + it 'returns not found response' do + get :active_metrics, project_params + + expect(response).to have_http_status(404) + end + end + end + end + + def project_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, project_id: project) + end +end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 952071af57f..b4eaab29fed 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -15,8 +15,8 @@ describe Projects::RawController do expect(response).to have_http_status(200) expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') - expect(response.header['Content-Disposition']). - to eq('inline') + expect(response.header['Content-Disposition']) + .to eq('inline') expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end end @@ -88,8 +88,8 @@ describe Projects::RawController do expect(response).to have_http_status(200) expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') - expect(response.header['Content-Disposition']). - to eq('inline') + expect(response.header['Content-Disposition']) + .to eq('inline') expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 23b463c0082..4dc227a36d4 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -67,8 +67,8 @@ describe Projects::ServicesController do put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params expect(response.status).to eq(200) - expect(JSON.parse(response.body)). - to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test') + expect(JSON.parse(response.body)) + .to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test') end end end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 8c23c46798e..ec0b7f8c967 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -46,7 +46,9 @@ describe Projects::SnippetsController do end context 'when signed in as the author' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders the snippet' do get :index, namespace_id: project.namespace, project_id: project @@ -57,7 +59,9 @@ describe Projects::SnippetsController do end context 'when signed in as a project member' do - before { sign_in(user2) } + before do + sign_in(user2) + end it 'renders the snippet' do get :index, namespace_id: project.namespace, project_id: project @@ -99,21 +103,21 @@ describe Projects::SnippetsController 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) + 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 { 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) + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } + .to change { SpamLog.count }.by(1) end it 'renders :new with recaptcha disabled' do @@ -179,8 +183,8 @@ describe Projects::SnippetsController do let(:visibility_level) { Snippet::PRIVATE } it 'updates the snippet' do - expect { update_snippet(title: 'Foo') }. - to change { snippet.reload.title }.to('Foo') + expect { update_snippet(title: 'Foo') } + .to change { snippet.reload.title }.to('Foo') end end @@ -188,13 +192,13 @@ describe Projects::SnippetsController do let(:visibility_level) { Snippet::PUBLIC } it 'rejects the shippet' do - expect { update_snippet(title: 'Foo') }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo') } + .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo') }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo') } + .to change { SpamLog.count }.by(1) end it 'renders :edit with recaptcha disabled' do @@ -233,13 +237,13 @@ describe Projects::SnippetsController do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the shippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } + .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } + .to change { SpamLog.count }.by(1) end it 'renders :edit with recaptcha disabled' do @@ -317,7 +321,9 @@ describe Projects::SnippetsController do end context 'when signed in as the author' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders the snippet' do get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param @@ -328,7 +334,9 @@ describe Projects::SnippetsController do end context 'when signed in as a project member' do - before { sign_in(user2) } + before do + sign_in(user2) + end it 'renders the snippet' do get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param @@ -349,7 +357,9 @@ describe Projects::SnippetsController do end context 'when signed in' do - before { sign_in(user) } + before do + sign_in(user) + end it 'responds with status 404' do get action, namespace_id: project.namespace, project_id: project, id: 42 diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index fc97bac64cd..c48f41ca12e 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -6,7 +6,9 @@ describe Projects::TagsController do let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') } describe 'GET index' do - before { get :index, namespace_id: project.namespace.to_param, project_id: project } + before do + get :index, namespace_id: project.namespace.to_param, project_id: project + end it 'returns the tags for the page' do expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0']) @@ -19,7 +21,9 @@ describe Projects::TagsController do end describe 'GET show' do - before { get :show, namespace_id: project.namespace.to_param, project_id: project, id: id } + before do + get :show, namespace_id: project.namespace.to_param, project_id: project, id: id + end context "valid tag" do let(:id) { 'v1.0.0' } diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index a43dad5756d..16cd2e076e5 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -82,8 +82,8 @@ describe Projects::TreeController do let(:id) { 'master/README.md' } it 'redirects' do redirect_url = "/#{project.path_with_namespace}/blob/master/README.md" - expect(subject). - to redirect_to(redirect_url) + expect(subject) + .to redirect_to(redirect_url) end end end @@ -106,8 +106,8 @@ describe Projects::TreeController do let(:branch_name) { 'master-test'} it 'redirects to the new directory' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/#{branch_name}/#{path}") + expect(subject) + .to redirect_to("/#{project.path_with_namespace}/tree/#{branch_name}/#{path}") expect(flash[:notice]).to eq('The directory has been successfully created.') end end @@ -117,8 +117,8 @@ describe Projects::TreeController do let(:branch_name) { 'master'} it 'does not allow overwriting of existing files' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/master") + expect(subject) + .to redirect_to("/#{project.path_with_namespace}/tree/master") expect(flash[:alert]).to eq('A file with this name already exists') end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4f6fc6691be..240a81367d0 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -29,7 +29,9 @@ describe ProjectsController do describe "GET show" do context "user not project member" do - before { sign_in(user) } + before do + sign_in(user) + end context "user does not have access to project" do let(:private_project) { create(:empty_project, :private) } @@ -108,7 +110,9 @@ describe ProjectsController do context "project with empty repo" do let(:empty_project) { create(:project_empty_repo, :public) } - before { sign_in(user) } + before do + sign_in(user) + end User.project_views.keys.each do |project_view| context "with #{project_view} view set" do @@ -128,7 +132,9 @@ describe ProjectsController do context "project with broken repo" do let(:empty_project) { create(:project_broken_repo, :public) } - before { sign_in(user) } + before do + sign_in(user) + end User.project_views.keys.each do |project_view| context "with #{project_view} view set" do diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 3173aae664c..a3708ad0908 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -18,7 +18,9 @@ describe SearchController do context 'on restricted projects' do context 'when signed out' do - before { sign_out(user) } + before do + sign_out(user) + end it "doesn't expose comments on issues" do project = create(:empty_project, :public, :issues_private) diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb index 954fc2eaf21..917bd44c91b 100644 --- a/spec/controllers/sent_notifications_controller_spec.rb +++ b/spec/controllers/sent_notifications_controller_spec.rb @@ -14,7 +14,9 @@ describe SentNotificationsController, type: :controller do describe 'GET unsubscribe' do context 'when the user is not logged in' do context 'when the force param is passed' do - before { get(:unsubscribe, id: sent_notification.reply_key, force: true) } + before do + get(:unsubscribe, id: sent_notification.reply_key, force: true) + end it 'unsubscribes the user' do expect(issue.subscribed?(user, project)).to be_falsey @@ -30,7 +32,9 @@ describe SentNotificationsController, type: :controller do end context 'when the force param is not passed' do - before { get(:unsubscribe, id: sent_notification.reply_key) } + before do + get(:unsubscribe, id: sent_notification.reply_key) + end it 'does not unsubscribe the user' do expect(issue.subscribed?(user, project)).to be_truthy @@ -47,10 +51,14 @@ describe SentNotificationsController, type: :controller do end context 'when the user is logged in' do - before { sign_in(user) } + before do + sign_in(user) + end context 'when the ID passed does not exist' do - before { get(:unsubscribe, id: sent_notification.reply_key.reverse) } + before do + get(:unsubscribe, id: sent_notification.reply_key.reverse) + end it 'does not unsubscribe the user' do expect(issue.subscribed?(user, project)).to be_truthy @@ -66,7 +74,9 @@ describe SentNotificationsController, type: :controller do end context 'when the force param is passed' do - before { get(:unsubscribe, id: sent_notification.reply_key, force: true) } + before do + get(:unsubscribe, id: sent_notification.reply_key, force: true) + end it 'unsubscribes the user' do expect(issue.subscribed?(user, project)).to be_falsey @@ -77,8 +87,8 @@ describe SentNotificationsController, type: :controller do end it 'redirects to the issue page' do - expect(response). - to redirect_to(namespace_project_issue_path(project.namespace, project, issue)) + expect(response) + .to redirect_to(namespace_project_issue_path(project.namespace, project, issue)) end end @@ -89,7 +99,10 @@ describe SentNotificationsController, type: :controller do end end let(:sent_notification) { create(:sent_notification, project: project, noteable: merge_request, recipient: user) } - before { get(:unsubscribe, id: sent_notification.reply_key) } + + before do + get(:unsubscribe, id: sent_notification.reply_key) + end it 'unsubscribes the user' do expect(merge_request.subscribed?(user, project)).to be_falsey @@ -100,8 +113,8 @@ describe SentNotificationsController, type: :controller do end it 'redirects to the merge request page' do - expect(response). - to redirect_to(namespace_project_merge_request_path(project.namespace, project, merge_request)) + expect(response) + .to redirect_to(namespace_project_merge_request_path(project.namespace, project, merge_request)) end end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index e87e24a33a1..bf922260b2f 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -9,8 +9,8 @@ describe SessionsController do context 'when auto sign-in is enabled' do before do stub_omniauth_setting(auto_sign_in_with_provider: :saml) - allow(controller).to receive(:omniauth_authorize_path).with(:user, :saml). - and_return('/saml') + allow(controller).to receive(:omniauth_authorize_path).with(:user, :saml) + .and_return('/saml') end context 'and no auto_sign_in param is passed' do @@ -89,8 +89,8 @@ describe SessionsController do context 'remember_me field' do it 'sets a remember_user_token cookie when enabled' do allow(controller).to receive(:find_user).and_return(user) - expect(controller). - to receive(:remember_me).with(user).and_call_original + expect(controller) + .to receive(:remember_me).with(user).and_call_original authenticate_2fa(remember_me: '1', otp_attempt: user.current_otp) @@ -142,7 +142,9 @@ describe SessionsController do end context 'when OTP is invalid' do - before { authenticate_2fa(otp_attempt: 'invalid') } + before do + authenticate_2fa(otp_attempt: 'invalid') + end it 'does not authenticate' do expect(subject.current_user).not_to eq user @@ -169,7 +171,9 @@ describe SessionsController do end context 'when OTP is invalid' do - before { authenticate_2fa(otp_attempt: 'invalid') } + before do + authenticate_2fa(otp_attempt: 'invalid') + end it 'does not authenticate' do expect(subject.current_user).not_to eq user @@ -224,8 +228,8 @@ describe SessionsController do it 'sets a remember_user_token cookie when enabled' do allow(U2fRegistration).to receive(:authenticate).and_return(true) allow(controller).to receive(:find_user).and_return(user) - expect(controller). - to receive(:remember_me).with(user).and_call_original + expect(controller) + .to receive(:remember_me).with(user).and_call_original authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}") @@ -258,8 +262,8 @@ describe SessionsController do it 'redirects correctly for referer on same host with params' do search_path = '/search?search=seed_project' - allow(controller.request).to receive(:referer). - and_return('http://%{host}%{path}' % { host: Gitlab.config.gitlab.host, path: search_path }) + allow(controller.request).to receive(:referer) + .and_return('http://%{host}%{path}' % { host: Gitlab.config.gitlab.host, path: search_path }) get(:new, redirect_to_referer: :yes) diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 9073c39f562..430d1208cd1 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -222,20 +222,20 @@ describe SnippetsController do 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) + 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 { create_snippet(visibility_level: Snippet::PUBLIC) } + .not_to change { Snippet.count } end it 'creates a spam log' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) + expect { create_snippet(visibility_level: Snippet::PUBLIC) } + .to change { SpamLog.count }.by(1) end it 'renders :new with recaptcha disabled' do @@ -296,8 +296,8 @@ describe SnippetsController do let(:visibility_level) { Snippet::PRIVATE } it 'updates the snippet' do - expect { update_snippet(title: 'Foo') }. - to change { snippet.reload.title }.to('Foo') + expect { update_snippet(title: 'Foo') } + .to change { snippet.reload.title }.to('Foo') end end @@ -305,13 +305,13 @@ describe SnippetsController do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } + .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } + .to change { SpamLog.count }.by(1) end it 'renders :edit with recaptcha disabled' do @@ -350,13 +350,13 @@ describe SnippetsController do let(:visibility_level) { Snippet::PUBLIC } it 'rejects the shippet' do - expect { update_snippet(title: 'Foo') }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo') } + .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo') }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo') } + .to change { SpamLog.count }.by(1) end it 'renders :edit with recaptcha disabled' do @@ -437,7 +437,9 @@ describe SnippetsController do end context 'when signed in user is the author' do - before { get :raw, id: personal_snippet.to_param } + before do + get :raw, id: personal_snippet.to_param + end it 'responds with status 200' do expect(assigns(:snippet)).to eq(personal_snippet) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index d33e2ba1e53..842d82cdbe9 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -43,7 +43,9 @@ describe UsersController do end context 'when logged in' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders show' do get :show, username: user.username @@ -62,7 +64,9 @@ describe UsersController do end context 'when logged in' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders 404' do get :show, username: 'nonexistent' diff --git a/spec/factories/application_settings.rb b/spec/factories/application_settings.rb new file mode 100644 index 00000000000..aef65e724c2 --- /dev/null +++ b/spec/factories/application_settings.rb @@ -0,0 +1,4 @@ +FactoryGirl.define do + factory :application_setting do + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0bb5a86d9b9..0cc498f0ce9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -194,8 +194,8 @@ FactoryGirl.define do trait :extended_options do options do { - image: 'ruby:2.1', - services: ['postgres'], + image: { name: 'ruby:2.1', entrypoint: '/bin/sh' }, + services: ['postgres', { name: 'docker:dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }], after_script: %w(ls date), artifacts: { name: 'artifacts_file', diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index e17e50db143..aef1c17a239 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -118,8 +118,8 @@ FactoryGirl.define do builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min - project.project_feature. - update_attributes!( + project.project_feature + .update_attributes!( wiki_access_level: evaluator.wiki_access_level, builds_access_level: builds_access_level, snippets_access_level: evaluator.snippets_access_level, diff --git a/spec/factories/services.rb b/spec/factories/services.rb index e7366a7fd1c..30bc25cf88a 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -25,6 +25,14 @@ FactoryGirl.define do }) end + factory :prometheus_service do + project factory: :empty_project + active true + properties({ + api_url: 'https://prometheus.example.com/' + }) + end + factory :jira_service do project factory: :empty_project active true diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb index 1e11fb756b2..5e6cd64c5c1 100644 --- a/spec/features/abuse_report_spec.rb +++ b/spec/features/abuse_report_spec.rb @@ -4,7 +4,7 @@ feature 'Abuse reports', feature: true do let(:another_user) { create(:user) } before do - login_as :user + gitlab_sign_in :user end scenario 'Report abuse' do diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 340884fc986..3a6e356b0b0 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -5,7 +5,7 @@ describe "Admin::AbuseReports", feature: true, js: true do context 'as an admin' do before do - login_as :admin + gitlab_sign_in :admin end describe 'if a user has been reported for abuse' do diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb index 16064d60ce2..c74336d8221 100644 --- a/spec/features/admin/admin_active_tab_spec.rb +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe 'admin active tab' do before do - login_as :admin + gitlab_sign_in :admin end shared_examples 'page has active tab' do |title| diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index 595366ce352..d8fd4319328 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -4,7 +4,7 @@ feature 'Admin Appearance', feature: true do let!(:appearance) { create(:appearance) } scenario 'Create new appearance' do - login_as :admin + gitlab_sign_in :admin visit admin_appearances_path fill_in 'appearance_title', with: 'MyCompany' @@ -20,7 +20,7 @@ feature 'Admin Appearance', feature: true do end scenario 'Preview appearance' do - login_as :admin + gitlab_sign_in :admin visit admin_appearances_path click_link "Preview" @@ -34,7 +34,7 @@ feature 'Admin Appearance', feature: true do end scenario 'Appearance logo' do - login_as :admin + gitlab_sign_in :admin visit admin_appearances_path attach_file(:appearance_logo, logo_fixture) @@ -46,7 +46,7 @@ feature 'Admin Appearance', feature: true do end scenario 'Header logos' do - login_as :admin + gitlab_sign_in :admin visit admin_appearances_path attach_file(:appearance_header_logo, logo_fixture) diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb index d6c63f66a9b..da063bf7b74 100644 --- a/spec/features/admin/admin_broadcast_messages_spec.rb +++ b/spec/features/admin/admin_broadcast_messages_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' feature 'Admin Broadcast Messages', feature: true do before do - login_as :admin + gitlab_sign_in :admin create(:broadcast_message, :expired, message: 'Migration to new server') visit admin_broadcast_messages_path end diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb index bee57472270..d9c4fc686b1 100644 --- a/spec/features/admin/admin_browse_spam_logs_spec.rb +++ b/spec/features/admin/admin_browse_spam_logs_spec.rb @@ -4,7 +4,7 @@ describe 'Admin browse spam logs' do let!(:spam_log) { create(:spam_log, description: 'abcde ' * 20) } before do - login_as :admin + gitlab_sign_in :admin end scenario 'Browse spam logs' do diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb index d880f3f07db..c734a2ef16d 100644 --- a/spec/features/admin/admin_browses_logs_spec.rb +++ b/spec/features/admin/admin_browses_logs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'Admin browses logs' do before do - login_as :admin + gitlab_sign_in :admin end it 'shows available log files' do diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 999ce3611b5..e767081f3e5 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'Admin Builds' do before do - login_as :admin + gitlab_sign_in :admin end describe 'GET /admin/builds' do diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb index dd14ffdb2ce..952e5475213 100644 --- a/spec/features/admin/admin_cohorts_spec.rb +++ b/spec/features/admin/admin_cohorts_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' feature 'Admin cohorts page', feature: true do before do - login_as :admin + gitlab_sign_in :admin end scenario 'See users count per month' do diff --git a/spec/features/admin/admin_conversational_development_index_spec.rb b/spec/features/admin/admin_conversational_development_index_spec.rb index 739ab907a29..b484677a6df 100644 --- a/spec/features/admin/admin_conversational_development_index_spec.rb +++ b/spec/features/admin/admin_conversational_development_index_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'Admin Conversational Development Index' do before do - login_as :admin + gitlab_sign_in :admin end context 'when usage ping is disabled' do diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb index 5f5fa4e932a..81cddd03f80 100644 --- a/spec/features/admin/admin_deploy_keys_spec.rb +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'admin deploy keys', type: :feature do let!(:another_deploy_key) { create(:another_deploy_key, public: true) } before do - login_as(:admin) + gitlab_sign_in(:admin) end it 'show all public deploy keys' do diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index e8e080ce3e2..679bf63e0fd 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -8,7 +8,7 @@ feature 'Admin disables Git access protocol', feature: true do background do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - login_as(admin) + gitlab_sign_in(admin) end context 'with HTTP disabled' do diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb index 71be66303d2..5437da29979 100644 --- a/spec/features/admin/admin_disables_two_factor_spec.rb +++ b/spec/features/admin/admin_disables_two_factor_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' feature 'Admin disables 2FA for a user', feature: true do scenario 'successfully', js: true do - login_as(:admin) + gitlab_sign_in(:admin) user = create(:user, :two_factor) edit_user(user) @@ -17,7 +17,7 @@ feature 'Admin disables 2FA for a user', feature: true do end scenario 'for a user without 2FA enabled' do - login_as(:admin) + gitlab_sign_in(:admin) user = create(:user) edit_user(user) diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index cf9d7bca255..8b0fafc5f07 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -6,7 +6,7 @@ feature 'Admin Groups', feature: true do let(:internal) { Gitlab::VisibilityLevel::INTERNAL } let(:user) { create :user } let!(:group) { create :group } - let!(:current_user) { login_as :admin } + let!(:current_user) { gitlab_sign_in :admin } before do stub_application_setting(default_group_visibility: internal) diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 523afa2318f..75093aa4167 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -5,7 +5,7 @@ feature "Admin Health Check", feature: true do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - login_as :admin + gitlab_sign_in :admin end describe '#show' do diff --git a/spec/features/admin/admin_hook_logs_spec.rb b/spec/features/admin/admin_hook_logs_spec.rb index 5b67f4de6ac..ec80c420c79 100644 --- a/spec/features/admin/admin_hook_logs_spec.rb +++ b/spec/features/admin/admin_hook_logs_spec.rb @@ -6,7 +6,7 @@ feature 'Admin::HookLogs', feature: true do let(:hook_log) { create(:web_hook_log, web_hook: system_hook, internal_error_message: 'some error') } before do - login_as :admin + gitlab_sign_in :admin end scenario 'show list of hook logs' do diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index 80f7ec43c06..c07c21bd6a1 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Admin::Hooks', feature: true do before do @project = create(:project) - login_as :admin + gitlab_sign_in :admin @system_hook = create(:system_hook) end diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb index a9251db13e5..bb40918bd22 100644 --- a/spec/features/admin/admin_labels_spec.rb +++ b/spec/features/admin/admin_labels_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'admin issues labels' do let!(:feature_label) { Label.create(title: 'feature', template: true) } before do - login_as :admin + gitlab_sign_in :admin end describe 'list' do diff --git a/spec/features/admin/admin_manage_applications_spec.rb b/spec/features/admin/admin_manage_applications_spec.rb index 0079125889b..ae41267e5fc 100644 --- a/spec/features/admin/admin_manage_applications_spec.rb +++ b/spec/features/admin/admin_manage_applications_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe 'admin manage applications', feature: true do before do - login_as :admin + gitlab_sign_in :admin end it do diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 9d205104ebe..a4ce3e1d5ee 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -6,7 +6,7 @@ describe "Admin::Projects", feature: true do let(:user) { create :user } let!(:project) { create(:project) } let!(:current_user) do - login_as :admin + gitlab_sign_in :admin end describe "GET /admin/projects" do @@ -57,8 +57,8 @@ describe "Admin::Projects", feature: true do before do create(:group, name: 'Web') - allow_any_instance_of(Projects::TransferService). - to receive(:move_uploads_to_new_namespace).and_return(true) + allow_any_instance_of(Projects::TransferService) + .to receive(:move_uploads_to_new_namespace).and_return(true) end it 'transfers project to group web', js: true do diff --git a/spec/features/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb index e8ecb70306b..2bfe401521b 100644 --- a/spec/features/admin/admin_requests_profiles_spec.rb +++ b/spec/features/admin/admin_requests_profiles_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Admin::RequestsProfilesController', feature: true do before do FileUtils.mkdir_p(Gitlab::RequestProfiler::PROFILES_DIR) - login_as(:admin) + gitlab_sign_in(:admin) end after do diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 5dcc7d35d82..6ad2d456b93 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -5,35 +5,58 @@ describe "Admin Runners" do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - login_as :admin + gitlab_sign_in :admin end describe "Runners page" do - before do - runner = FactoryGirl.create(:ci_runner, contacted_at: Time.now) - pipeline = FactoryGirl.create(:ci_pipeline) - FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id) - visit admin_runners_path - end + let(:pipeline) { create(:ci_pipeline) } + + context "when there are runners" do + before do + runner = FactoryGirl.create(:ci_runner, contacted_at: Time.now) + FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id) + visit admin_runners_path + end + + it 'has all necessary texts' do + expect(page).to have_text "To register a new Runner" + expect(page).to have_text "Runners with last contact more than a minute ago: 1" + end + + describe 'search' do + before do + FactoryGirl.create :ci_runner, description: 'runner-foo' + FactoryGirl.create :ci_runner, description: 'runner-bar' + end + + it 'shows correct runner when description matches' do + search_form = find('#runners-search') + search_form.fill_in 'search', with: 'runner-foo' + search_form.click_button 'Search' - it 'has all necessary texts' do - expect(page).to have_text "To register a new Runner" - expect(page).to have_text "Runners with last contact more than a minute ago: 1" + expect(page).to have_content("runner-foo") + expect(page).not_to have_content("runner-bar") + end + + it 'shows no runner when description does not match' do + search_form = find('#runners-search') + search_form.fill_in 'search', with: 'runner-baz' + search_form.click_button 'Search' + + expect(page).to have_text 'No runners found' + end + end end - describe 'search' do + context "when there are no runners" do before do - FactoryGirl.create :ci_runner, description: 'runner-foo' - FactoryGirl.create :ci_runner, description: 'runner-bar' - - search_form = find('#runners-search') - search_form.fill_in 'search', with: 'runner-foo' - search_form.click_button 'Search' + visit admin_runners_path end - it 'shows correct runner' do - expect(page).to have_content("runner-foo") - expect(page).not_to have_content("runner-bar") + it 'has all necessary texts including no runner message' do + expect(page).to have_text "To register a new Runner" + expect(page).to have_text "Runners with last contact more than a minute ago: 0" + expect(page).to have_text 'No runners found' end end end @@ -134,7 +157,10 @@ describe "Admin Runners" do describe 'runners registration token' do let!(:token) { current_application_settings.runners_registration_token } - before { visit admin_runners_path } + + before do + visit admin_runners_path + end it 'has a registration token' do expect(page).to have_content("Registration token is #{token}") diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 5099441dce2..2d6565e6d3b 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -5,7 +5,7 @@ feature 'Admin updates settings', feature: true do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - login_as :admin + gitlab_sign_in :admin visit admin_application_settings_path end @@ -20,10 +20,15 @@ feature 'Admin updates settings', feature: true do uncheck 'Gravatar enabled' fill_in 'Home page URL', with: 'https://about.gitlab.com/' fill_in 'Help page text', with: 'Example text' + check 'Hide marketing-related entries from help' + fill_in 'Support page URL', with: 'http://example.com/help' click_button 'Save' expect(current_application_settings.gravatar_enabled).to be_falsey expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" + expect(current_application_settings.help_page_text).to eq "Example text" + expect(current_application_settings.help_page_hide_commercial_content).to be_truthy + expect(current_application_settings.help_page_support_url).to eq "http://example.com/help" expect(page).to have_content "Application settings saved successfully" end diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb index 15482347886..4efc7f0eb48 100644 --- a/spec/features/admin/admin_system_info_spec.rb +++ b/spec/features/admin/admin_system_info_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'Admin System Info' do before do - login_as :admin + gitlab_sign_in :admin end describe 'GET /admin/system_info' do diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb index 0fb4baeb71c..231c094c91d 100644 --- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb +++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb @@ -12,7 +12,9 @@ describe 'Admin > Users > Impersonation Tokens', feature: true, js: true do find(".table.inactive-tokens") end - before { login_as(admin) } + before do + gitlab_sign_in(admin) + end describe "token creation" do it "allows creation of a token" do diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 301a47169a4..6dbc697642f 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -5,7 +5,7 @@ describe "Admin::Users", feature: true do create(:omniauth_user, provider: 'twitter', extern_uid: '123456') end - let!(:current_user) { login_as :admin } + let!(:current_user) { gitlab_sign_in :admin } describe "GET /admin/users" do before do @@ -78,10 +78,10 @@ describe "Admin::Users", feature: true do it "applies defaults to user" do click_button "Create user" user = User.find_by(username: 'bang') - expect(user.projects_limit). - to eq(Gitlab.config.gitlab.default_projects_limit) - expect(user.can_create_group). - to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.projects_limit) + .to eq(Gitlab.config.gitlab.default_projects_limit) + expect(user.can_create_group) + .to eq(Gitlab.config.gitlab.default_can_create_group) end it "creates user with valid data" do @@ -124,7 +124,10 @@ describe "Admin::Users", feature: true do describe 'Impersonation' do let(:another_user) { create(:user) } - before { visit admin_user_path(another_user) } + + before do + visit admin_user_path(another_user) + end context 'before impersonating' do it 'shows impersonate button for other users' do @@ -149,7 +152,9 @@ describe "Admin::Users", feature: true do end context 'when impersonating' do - before { click_link 'Impersonate' } + before do + click_link 'Impersonate' + end it 'logs in as the user when impersonate is clicked' do expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username) diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index ab5c42365fe..91d70435db8 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -5,7 +5,7 @@ feature 'Admin uses repository checks', feature: true do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - login_as :admin + gitlab_sign_in :admin end scenario 'to trigger a single check' do diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 1df058b023c..2f4bb45d74b 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -35,8 +35,8 @@ describe "Dashboard Feed", feature: true do end it "has issue comment event" do - expect(body). - to have_content("#{user.name} commented on issue ##{issue.iid}") + expect(body) + .to have_content("#{user.name} commented on issue ##{issue.iid}") end end end diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index a61231ea254..b94ad973fed 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -15,11 +15,11 @@ describe 'Issues Feed', feature: true do context 'when authenticated' do it 'renders atom feed' do - login_with user + gitlab_sign_in user visit namespace_project_issues_path(project.namespace, project, :atom) - expect(response_headers['Content-Type']). - to have_content('application/atom+xml') + expect(response_headers['Content-Type']) + .to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{project.name} issues") expect(body).to have_selector('author email', text: issue.author_public_email) expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email) @@ -33,8 +33,8 @@ describe 'Issues Feed', feature: true do visit namespace_project_issues_path(project.namespace, project, :atom, private_token: user.private_token) - expect(response_headers['Content-Type']). - to have_content('application/atom+xml') + expect(response_headers['Content-Type']) + .to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{project.name} issues") expect(body).to have_selector('author email', text: issue.author_public_email) expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email) @@ -48,8 +48,8 @@ describe 'Issues Feed', feature: true do visit namespace_project_issues_path(project.namespace, project, :atom, rss_token: user.rss_token) - expect(response_headers['Content-Type']). - to have_content('application/atom+xml') + expect(response_headers['Content-Type']) + .to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{project.name} issues") expect(body).to have_selector('author email', text: issue.author_public_email) expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email) diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index fae5aaa52bd..44ae7204bcf 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -55,8 +55,8 @@ describe "User Feed", feature: true do end it 'has issue comment event' do - expect(body). - to have_content("#{safe_name} commented on issue ##{issue.iid}") + expect(body) + .to have_content("#{safe_name} commented on issue ##{issue.iid}") end it 'has XHTML summaries in issue descriptions' do diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb index 1cf7396bbac..74f5f70702a 100644 --- a/spec/features/auto_deploy_spec.rb +++ b/spec/features/auto_deploy_spec.rb @@ -7,7 +7,7 @@ describe 'Auto deploy' do before do create :kubernetes_service, project: project project.team << [user, :master] - login_as user + gitlab_sign_in user end context 'when no deployment service is active' do diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 2b8edac4f10..ba58af22841 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -14,7 +14,7 @@ describe 'Issue Boards add issue modal', :feature, :js do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_board_path(project.namespace, project, board) wait_for_requests diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index c80453b8227..87fc31d414c 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -12,7 +12,7 @@ describe 'Issue Boards', feature: true, js: true do project.team << [user, :master] project.team << [user2, :master] - login_as(user) + gitlab_sign_in(user) end context 'no lists' do @@ -247,13 +247,13 @@ describe 'Issue Boards', feature: true, js: true do end it 'issue moves from closed' do - drag(list_from_index: 3, list_to_index: 2) - - expect(find('.board:nth-child(3)')).to have_content(issue8.title) + drag(list_from_index: 2, list_to_index: 3) wait_for_board_cards(2, 8) - wait_for_board_cards(3, 3) - wait_for_board_cards(4, 0) + wait_for_board_cards(3, 1) + wait_for_board_cards(4, 2) + + expect(find('.board:nth-child(4)')).to have_content(issue8.title) end context 'issue card' do @@ -519,7 +519,7 @@ describe 'Issue Boards', feature: true, js: true do context 'signed out user' do before do - logout + gitlab_sign_out visit namespace_project_board_path(project.namespace, project, board) wait_for_requests end @@ -542,8 +542,8 @@ describe 'Issue Boards', feature: true, js: true do before do project.team << [user_guest, :guest] - logout - login_as(user_guest) + gitlab_sign_out + gitlab_sign_in(user_guest) visit namespace_project_board_path(project.namespace, project, board) wait_for_requests end diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 1c289993e28..1e620061e5e 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -15,7 +15,7 @@ describe 'Issue Boards', :feature, :js do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'un-ordered issues' do diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb index c2167ba12cd..ed3b38e6a7e 100644 --- a/spec/features/boards/keyboard_shortcut_spec.rb +++ b/spec/features/boards/keyboard_shortcut_spec.rb @@ -6,7 +6,7 @@ describe 'Issue Boards shortcut', feature: true, js: true do before do create(:board, project: project) - login_as :admin + gitlab_sign_in :admin visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index b6de6143354..8899e1ef5e5 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -12,7 +12,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end it 'shows empty state when no results found' do diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 056224dc436..77cd87d6601 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -10,7 +10,7 @@ describe 'Issue Boards new issue', feature: true, js: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_board_path(project.namespace, project, board) wait_for_requests @@ -19,18 +19,18 @@ describe 'Issue Boards new issue', feature: true, js: true do end it 'displays new issue button' do - expect(first('.board')).to have_selector('.board-issue-count-holder .btn', count: 1) + expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) end it 'does not display new issue button in closed list' do page.within('.board:nth-child(3)') do - expect(page).not_to have_selector('.board-issue-count-holder .btn') + expect(page).not_to have_selector('.issue-count-badge-add-button') end end it 'shows form when clicking button' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click expect(page).to have_selector('.board-new-issue-form') end @@ -38,7 +38,7 @@ describe 'Issue Boards new issue', feature: true, js: true do it 'hides form when clicking cancel' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click expect(page).to have_selector('.board-new-issue-form') @@ -50,7 +50,7 @@ describe 'Issue Boards new issue', feature: true, js: true do it 'creates new issue' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click end page.within(first('.board-new-issue-form')) do @@ -60,14 +60,14 @@ describe 'Issue Boards new issue', feature: true, js: true do wait_for_requests - page.within(first('.board .board-issue-count')) do + page.within(first('.board .issue-count-badge-count')) do expect(page).to have_content('1') end end it 'shows sidebar when creating new issue' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click end page.within(first('.board-new-issue-form')) do @@ -88,7 +88,7 @@ describe 'Issue Boards new issue', feature: true, js: true do end it 'does not display new issue button' do - expect(page).to have_selector('.board-issue-count-holder .btn', count: 0) + expect(page).to have_selector('.issue-count-badge-add-button', count: 0) end end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 235e4899707..301c243febd 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -20,7 +20,7 @@ describe 'Issue Boards', feature: true, js: true do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_board_path(project.namespace, project, board) wait_for_requests diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb index 4cd05010a93..d57ae6a71e7 100644 --- a/spec/features/boards/sub_group_project_spec.rb +++ b/spec/features/boards/sub_group_project_spec.rb @@ -13,7 +13,7 @@ describe 'Sub-group project issue boards', :feature, :js do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_board_path(project.namespace, project, board) wait_for_requests diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 1b6d8439f92..b2e72fc7dee 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -68,7 +68,7 @@ feature 'Contributions Calendar', :feature, :js do end before do - login_as user + gitlab_sign_in user end describe 'calendar day selection' do diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index 3ebc432206a..de16ee3e567 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'CI Lint', js: true do before do - login_as :user + gitlab_sign_in :user end describe 'YAML parsing' do diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 2772f05982a..0373f649ee8 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -4,10 +4,11 @@ describe 'Commits' do include CiStatusHelper let(:project) { create(:project, :repository) } + let(:user) { create(:user) } describe 'CI' do before do - login_as :user + sign_in(user) stub_ci_pipeline_to_return_yaml_file end @@ -27,7 +28,7 @@ describe 'Commits' do let!(:status) { create(:generic_commit_status, pipeline: pipeline) } before do - project.team << [@user, :reporter] + project.team << [user, :reporter] end describe 'Commit builds' do @@ -52,7 +53,7 @@ describe 'Commits' do context 'when logged as developer' do before do - project.team << [@user, :developer] + project.team << [user, :developer] end describe 'Project commits' do @@ -146,7 +147,7 @@ describe 'Commits' do context "when logged as reporter" do before do - project.team << [@user, :reporter] + project.team << [user, :reporter] build.update_attributes(artifacts_file: artifacts_file) visit ci_status_path(pipeline) end @@ -187,11 +188,10 @@ describe 'Commits' do context 'viewing commits for a branch' do let(:branch_name) { 'master' } - let(:user) { create(:user) } before do project.team << [user, :master] - login_with(user) + sign_in(user) visit namespace_project_commits_path(project.namespace, project, branch_name) end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index fa7adbe71ea..80d16539d5a 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -9,7 +9,7 @@ describe "Container Registry" do end before do - login_as(user) + gitlab_sign_in(user) project.add_developer(user) stub_container_registry_config(enabled: true) stub_container_registry_tags(repository: :any, tags: []) diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 740f60c05cc..005c88f6bab 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -6,7 +6,7 @@ describe 'Copy as GFM', feature: true, js: true do include ActionView::Helpers::JavaScriptHelper before do - login_as :admin + gitlab_sign_in :admin end describe 'Copying rendered GFM' do diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index b416bbd3c79..5a7ea975455 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -14,7 +14,7 @@ feature 'Cycle Analytics', feature: true, js: true do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_cycle_analytics_path(project.namespace, project) wait_for_requests @@ -38,7 +38,7 @@ feature 'Cycle Analytics', feature: true, js: true do create_cycle deploy_master - login_as(user) + gitlab_sign_in(user) visit namespace_project_cycle_analytics_path(project.namespace, project) end @@ -70,7 +70,7 @@ feature 'Cycle Analytics', feature: true, js: true do user.update_attribute(:preferred_language, 'es') project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_cycle_analytics_path(project.namespace, project) wait_for_requests end @@ -93,7 +93,7 @@ feature 'Cycle Analytics', feature: true, js: true do create_cycle deploy_master - login_as(guest) + gitlab_sign_in(guest) visit namespace_project_cycle_analytics_path(project.namespace, project) wait_for_requests end diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb index ae750be4d4a..f7ddded10c1 100644 --- a/spec/features/dashboard/active_tab_spec.rb +++ b/spec/features/dashboard/active_tab_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe 'Dashboard Active Tab', js: true, feature: true do before do - login_as :user + gitlab_sign_in :user end shared_examples 'page has active tab' do |title| diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb index 0764044260e..1e9cabe7850 100644 --- a/spec/features/dashboard/activity_spec.rb +++ b/spec/features/dashboard/activity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe 'Dashboard Activity', feature: true do before do - login_as(create :user) + gitlab_sign_in(create :user) visit activity_dashboard_path end diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb index f33bcbb5318..a5ba3e7e3cf 100644 --- a/spec/features/dashboard/archived_projects_spec.rb +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -9,7 +9,7 @@ RSpec.describe 'Dashboard Archived Project', feature: true do project.team << [user, :master] archived_project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit dashboard_projects_path end diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 1793e323588..6931d0a840e 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -4,7 +4,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:created_date) { Date.yesterday.to_time } - let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P') } + let(:expected_format) { created_date.in_time_zone.strftime('%b %-d, %Y %l:%M%P') } context 'on the activity tab' do before do @@ -13,7 +13,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do Event.create( project: project, author_id: user.id, action: Event::JOINED, updated_at: created_date, created_at: created_date) - login_as user + gitlab_sign_in user visit user_path(user) wait_for_requests() @@ -30,7 +30,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do project.team << [user, :master] create(:snippet, author: user, updated_at: created_date, created_at: created_date) - login_as user + gitlab_sign_in user visit user_snippets_path(user) wait_for_requests() diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb index 8e20fdec8ad..2f7245950ec 100644 --- a/spec/features/dashboard/group_spec.rb +++ b/spec/features/dashboard/group_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe 'Dashboard Group', feature: true do before do - login_as(:user) + gitlab_sign_in(:user) end it 'creates new group', js: true do diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index 7eb254f8451..e520027bc38 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -10,7 +10,7 @@ describe 'Dashboard Groups page', js: true, feature: true do group.add_owner(user) nested_group.add_owner(user) - login_as(user) + gitlab_sign_in(user) visit dashboard_groups_path expect(page).to have_content(group.full_name) @@ -23,7 +23,7 @@ describe 'Dashboard Groups page', js: true, feature: true do group.add_owner(user) nested_group.add_owner(user) - login_as(user) + gitlab_sign_in(user) visit dashboard_groups_path end @@ -58,7 +58,7 @@ describe 'Dashboard Groups page', js: true, feature: true do group.add_owner(user) subgroup.add_owner(user) - login_as(user) + gitlab_sign_in(user) visit dashboard_groups_path end @@ -98,7 +98,7 @@ describe 'Dashboard Groups page', js: true, feature: true do allow(Kaminari.config).to receive(:default_per_page).and_return(1) - login_as(user) + gitlab_sign_in(user) visit dashboard_groups_path end diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb index 2803f7ec62b..25b0f40c9cd 100644 --- a/spec/features/dashboard/help_spec.rb +++ b/spec/features/dashboard/help_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe 'Dashboard Help', feature: true do before do - login_as(:user) + gitlab_sign_in(:user) end it 'renders correctly markdown' do diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index 354267dbee7..8a8a20fd5b1 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -9,7 +9,7 @@ describe 'Navigation bar counter', feature: true, caching: true do before do issue.assignees = [user] merge_request.update(assignee: user) - login_as(user) + gitlab_sign_in(user) end it 'reflects dashboard issues count' do diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 2cea6b1563e..a57962abbda 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Dashboard Issues', feature: true do before do [project, project_with_issues_disabled].each { |project| project.team << [current_user, :master] } - login_as(current_user) + gitlab_sign_in(current_user) visit issues_dashboard_path(assignee_id: current_user.id) end @@ -59,6 +59,11 @@ RSpec.describe 'Dashboard Issues', feature: true do expect(page).to have_content(other_issue.title) end + it 'state filter tabs work' do + find('#state-closed').click + expect(page).to have_current_path(issues_dashboard_url(assignee_id: current_user.id, scope: 'all', state: 'closed'), url: true) + end + it_behaves_like "it has an RSS button with current_user's RSS token" it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb index 4cff12de854..88bbb9e75b9 100644 --- a/spec/features/dashboard/label_filter_spec.rb +++ b/spec/features/dashboard/label_filter_spec.rb @@ -11,7 +11,7 @@ describe 'Dashboard > label filter', feature: true, js: true do project.labels << label project2.labels << label2 - login_as(user) + gitlab_sign_in(user) visit issues_dashboard_path end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 9cebe52c444..bb1fb5b3feb 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -1,44 +1,113 @@ require 'spec_helper' -describe 'Dashboard Merge Requests' do +feature 'Dashboard Merge Requests' do + include FilterItemSelectHelper + let(:current_user) { create :user } let(:project) { create(:empty_project) } - let(:project_with_merge_requests_disabled) { create(:empty_project, :merge_requests_disabled) } - before do - [project, project_with_merge_requests_disabled].each { |project| project.team << [current_user, :master] } + let(:public_project) { create(:empty_project, :public, :repository) } + let(:forked_project) { Projects::ForkService.new(public_project, current_user).execute } - login_as(current_user) + before do + project.add_master(current_user) + sign_in(current_user) end - describe 'new merge request dropdown' do - before { visit merge_requests_dashboard_path } + context 'new merge request dropdown' do + let(:project_with_disabled_merge_requests) { create(:empty_project, :merge_requests_disabled) } + + before do + project_with_disabled_merge_requests.add_master(current_user) + visit merge_requests_dashboard_path + end it 'shows projects only with merge requests feature enabled', js: true do find('.new-project-item-select-button').trigger('click') page.within('.select2-results') do expect(page).to have_content(project.name_with_namespace) - expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace) + expect(page).not_to have_content(project_with_disabled_merge_requests.name_with_namespace) end end end - it 'should show an empty state' do - visit merge_requests_dashboard_path(assignee_id: current_user.id) + context 'no merge requests exist' do + it 'shows an empty state' do + visit merge_requests_dashboard_path(assignee_id: current_user.id) - expect(page).to have_selector('.empty-state') + expect(page).to have_selector('.empty-state') + end end - context 'if there are merge requests' do - before do - create(:merge_request, assignee: current_user, source_project: project) + context 'merge requests exist' do + let!(:assigned_merge_request) do + create(:merge_request, assignee: current_user, target_project: project, source_project: project) + end + + let!(:assigned_merge_request_from_fork) do + create(:merge_request, + source_branch: 'markdown', assignee: current_user, + target_project: public_project, source_project: forked_project + ) + end + + let!(:authored_merge_request) do + create(:merge_request, + source_branch: 'markdown', author: current_user, + target_project: project, source_project: project + ) + end + let!(:authored_merge_request_from_fork) do + create(:merge_request, + source_branch: 'feature_conflict', + author: current_user, + target_project: public_project, source_project: forked_project + ) + end + + let!(:other_merge_request) do + create(:merge_request, + source_branch: 'fix', + target_project: project, source_project: project + ) + end + + before do visit merge_requests_dashboard_path(assignee_id: current_user.id) end - it 'should not show an empty state' do - expect(page).not_to have_selector('.empty-state') + it 'shows assigned merge requests' do + expect(page).to have_content(assigned_merge_request.title) + expect(page).to have_content(assigned_merge_request_from_fork.title) + + expect(page).not_to have_content(authored_merge_request.title) + expect(page).not_to have_content(authored_merge_request_from_fork.title) + expect(page).not_to have_content(other_merge_request.title) + end + + it 'shows authored merge requests', js: true do + filter_item_select('Any Assignee', '.js-assignee-search') + filter_item_select(current_user.to_reference, '.js-author-search') + + expect(page).to have_content(authored_merge_request.title) + expect(page).to have_content(authored_merge_request_from_fork.title) + + expect(page).not_to have_content(assigned_merge_request.title) + expect(page).not_to have_content(assigned_merge_request_from_fork.title) + expect(page).not_to have_content(other_merge_request.title) + end + + it 'shows all merge requests', js: true do + filter_item_select('Any Assignee', '.js-assignee-search') + filter_item_select('Any Author', '.js-author-search') + + expect(page).to have_content(authored_merge_request.title) + expect(page).to have_content(authored_merge_request_from_fork.title) + expect(page).to have_content(assigned_merge_request.title) + expect(page).to have_content(assigned_merge_request_from_fork.title) + expect(page).to have_content(other_merge_request.title) end end end diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb index b5b92c36895..b0e4036f27c 100644 --- a/spec/features/dashboard/milestone_filter_spec.rb +++ b/spec/features/dashboard/milestone_filter_spec.rb @@ -1,15 +1,17 @@ require 'spec_helper' -describe 'Dashboard > milestone filter', :feature, :js do +feature 'Dashboard > milestone filter', :feature, :js do + include FilterItemSelectHelper + let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } - let(:milestone) { create(:milestone, title: "v1.0", project: project) } - let(:milestone2) { create(:milestone, title: "v2.0", project: project) } + let(:milestone) { create(:milestone, title: 'v1.0', project: project) } + let(:milestone2) { create(:milestone, title: 'v2.0', project: project) } let!(:issue) { create :issue, author: user, project: project, milestone: milestone } let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 } before do - login_as(user) + gitlab_sign_in(user) visit issues_dashboard_path(author_id: user.id) end @@ -22,17 +24,11 @@ describe 'Dashboard > milestone filter', :feature, :js do end context 'filtering by milestone' do - milestone_select = '.js-milestone-select' + milestone_select_selector = '.js-milestone-select' before do - find(milestone_select).click - wait_for_requests - - page.within('.dropdown-content') do - click_link 'v1.0' - end - - find(milestone_select).click + filter_item_select('v1.0', milestone_select_selector) + find(milestone_select_selector).click wait_for_requests end @@ -49,7 +45,7 @@ describe 'Dashboard > milestone filter', :feature, :js do expect(find('.milestone-filter')).not_to have_selector('.dropdown.open') - find(milestone_select).click + find(milestone_select_selector).click expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) expect(find('.dropdown-content a.is-active')).to have_content('v1.0') diff --git a/spec/features/dashboard/milestone_tabs_spec.rb b/spec/features/dashboard/milestone_tabs_spec.rb index 0c7b992c500..cc4193b180f 100644 --- a/spec/features/dashboard/milestone_tabs_spec.rb +++ b/spec/features/dashboard/milestone_tabs_spec.rb @@ -15,7 +15,7 @@ describe 'Dashboard milestone tabs', :js, :feature do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit dashboard_milestone_path(milestone.safe_title, title: milestone.title) end @@ -23,7 +23,7 @@ describe 'Dashboard milestone tabs', :js, :feature do it 'loads merge requests async' do click_link 'Merge Requests' - expect(page).to have_selector('.merge_requests-sortable-list') + expect(page).to have_selector('.milestone-merge_requests-list') end it 'loads participants async' do diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index cdf919af9b5..0ba87d921d0 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -17,19 +17,25 @@ feature 'Project member activity', feature: true, js: true do subject { page.find(".event-title").text } context 'when a user joins the project' do - before { visit_activities_and_wait_with_event(Event::JOINED) } + before do + visit_activities_and_wait_with_event(Event::JOINED) + end it { is_expected.to eq("#{user.name} joined project") } end context 'when a user leaves the project' do - before { visit_activities_and_wait_with_event(Event::LEFT) } + before do + visit_activities_and_wait_with_event(Event::LEFT) + end it { is_expected.to eq("#{user.name} left project") } end context 'when a users membership expires for the project' do - before { visit_activities_and_wait_with_event(Event::EXPIRED) } + before do + visit_activities_and_wait_with_event(Event::EXPIRED) + end it "presents the correct message" do message = "#{user.name} removed due to membership expiration from project" diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 3568954a548..f29186f368d 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Dashboard Projects', feature: true do before do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) end it 'shows the project the user in a member of in the list' do @@ -15,13 +15,25 @@ RSpec.describe 'Dashboard Projects', feature: true do expect(page).to have_content('awesome stuff') end - it 'shows the last_activity_at attribute as the update date' do - now = Time.now - project.update_column(:last_activity_at, now) + context 'when last_repository_updated_at, last_activity_at and update_at are present' do + it 'shows the last_repository_updated_at attribute as the update date' do + project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago) - visit dashboard_projects_path + visit dashboard_projects_path + + expect(page).to have_xpath("//time[@datetime='#{project.last_repository_updated_at.getutc.iso8601}']") + end + end - expect(page).to have_xpath("//time[@datetime='#{now.getutc.iso8601}']") + context 'when last_repository_updated_at and last_activity_at are missing' do + it 'shows the updated_at attribute as the update date' do + project.update_attributes!(last_repository_updated_at: nil, last_activity_at: nil) + project.touch + + visit dashboard_projects_path + + expect(page).to have_xpath("//time[@datetime='#{project.updated_at.getutc.iso8601}']") + end end context 'when on Starred projects tab' do diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index 349b948eaee..525b0e1b210 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' feature 'Dashboard shortcuts', :feature, :js do context 'logged in' do before do - login_as :user + gitlab_sign_in :user visit root_dashboard_path end diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb index c6ba118220a..0c069ae5cf0 100644 --- a/spec/features/dashboard/snippets_spec.rb +++ b/spec/features/dashboard/snippets_spec.rb @@ -6,7 +6,7 @@ describe 'Dashboard snippets', feature: true do let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } before do allow(Snippet).to receive(:default_per_page).and_return(1) - login_as(project.owner) + gitlab_sign_in(project.owner) visit dashboard_snippets_path end @@ -25,7 +25,7 @@ describe 'Dashboard snippets', feature: true do end before do - login_as(user) + gitlab_sign_in(user) visit dashboard_snippets_path end diff --git a/spec/features/todos/target_state_spec.rb b/spec/features/dashboard/todos/target_state_spec.rb index 32fa88a2b21..030a86d1c01 100644 --- a/spec/features/todos/target_state_spec.rb +++ b/spec/features/dashboard/todos/target_state_spec.rb @@ -1,12 +1,12 @@ require 'rails_helper' -feature 'Todo target states', feature: true do +feature 'Dashboard > Todo target states' do let(:user) { create(:user) } let(:author) { create(:user) } - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:project) { create(:project, :public) } before do - login_as user + sign_in(user) end scenario 'on a closed issue todo has closed label' do @@ -30,7 +30,7 @@ feature 'Todo target states', feature: true do end scenario 'on a merged merge request todo has merged label' do - mr_merged = create(:merge_request, :simple, author: user, state: 'merged') + mr_merged = create(:merge_request, :simple, :merged, author: user) create_todo mr_merged visit dashboard_todos_path @@ -40,7 +40,7 @@ feature 'Todo target states', feature: true do end scenario 'on a closed merge request todo has closed label' do - mr_closed = create(:merge_request, :simple, author: user, state: 'closed') + mr_closed = create(:merge_request, :simple, :closed, author: user) create_todo mr_closed visit dashboard_todos_path diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb index bbfa4e08379..0a363259fe7 100644 --- a/spec/features/todos/todos_filtering_spec.rb +++ b/spec/features/dashboard/todos/todos_filtering_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Dashboard > User filters todos', feature: true, js: true do +feature 'Dashboard > User filters todos', js: true do let(:user_1) { create(:user, username: 'user_1', name: 'user_1') } let(:user_2) { create(:user, username: 'user_2', name: 'user_2') } @@ -17,7 +17,7 @@ describe 'Dashboard > User filters todos', feature: true, js: true do project_1.team << [user_1, :developer] project_2.team << [user_1, :developer] - login_as(user_1) + sign_in(user_1) visit dashboard_todos_path end @@ -34,7 +34,7 @@ describe 'Dashboard > User filters todos', feature: true, js: true do expect(page).not_to have_content project_2.name_with_namespace end - context "Author filter" do + context 'Author filter' do it 'filters by author' do click_button 'Author' @@ -49,18 +49,18 @@ describe 'Dashboard > User filters todos', feature: true, js: true do expect(find('.todos-list')).not_to have_content 'issue' end - it "shows only authors of existing todos" do + it 'shows only authors of existing todos' do click_button 'Author' within '.dropdown-menu-author' do - # It should contain two users + "Any Author" + # It should contain two users + 'Any Author' expect(page).to have_selector('.dropdown-menu-user-link', count: 3) expect(page).to have_content(user_1.name) expect(page).to have_content(user_2.name) end end - it "shows only authors of existing done todos" do + it 'shows only authors of existing done todos' do user_3 = create :user user_4 = create :user create(:todo, user: user_1, author: user_3, project: project_1, target: issue, action: 1, state: :done) @@ -74,7 +74,7 @@ describe 'Dashboard > User filters todos', feature: true, js: true do click_button 'Author' within '.dropdown-menu-author' do - # It should contain two users + "Any Author" + # It should contain two users + 'Any Author' expect(page).to have_selector('.dropdown-menu-user-link', count: 3) expect(page).to have_content(user_3.name) expect(page).to have_content(user_4.name) diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb index 4d5bd476301..5858f4aa101 100644 --- a/spec/features/todos/todos_sorting_spec.rb +++ b/spec/features/dashboard/todos/todos_sorting_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Dashboard > User sorts todos", feature: true do +feature 'Dashboard > User sorts todos' do let(:user) { create(:user) } let(:project) { create(:empty_project) } @@ -8,7 +8,9 @@ describe "Dashboard > User sorts todos", feature: true do let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end context 'sort options' do let(:issue_1) { create(:issue, title: 'issue_1', project: project) } @@ -16,7 +18,7 @@ describe "Dashboard > User sorts todos", feature: true do let(:issue_3) { create(:issue, title: 'issue_3', project: project) } let(:issue_4) { create(:issue, title: 'issue_4', project: project) } - let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") } + let!(:merge_request_1) { create(:merge_request, source_project: project, title: 'merge_request_1') } before do create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago) @@ -30,41 +32,41 @@ describe "Dashboard > User sorts todos", feature: true do issue_2.labels << label_3 issue_1.labels << label_2 - login_as(user) + sign_in(user) visit dashboard_todos_path end - it "sorts with oldest created todos first" do - click_link "Last created" + it 'sorts with oldest created todos first' do + click_link 'Last created' results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("merge_request_1") - expect(results_list.all('p')[1]).to have_content("issue_1") - expect(results_list.all('p')[2]).to have_content("issue_3") - expect(results_list.all('p')[3]).to have_content("issue_2") - expect(results_list.all('p')[4]).to have_content("issue_4") + expect(results_list.all('p')[0]).to have_content('merge_request_1') + expect(results_list.all('p')[1]).to have_content('issue_1') + expect(results_list.all('p')[2]).to have_content('issue_3') + expect(results_list.all('p')[3]).to have_content('issue_2') + expect(results_list.all('p')[4]).to have_content('issue_4') end - it "sorts with newest created todos first" do - click_link "Oldest created" + it 'sorts with newest created todos first' do + click_link 'Oldest created' results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("issue_4") - expect(results_list.all('p')[1]).to have_content("issue_2") - expect(results_list.all('p')[2]).to have_content("issue_3") - expect(results_list.all('p')[3]).to have_content("issue_1") - expect(results_list.all('p')[4]).to have_content("merge_request_1") + expect(results_list.all('p')[0]).to have_content('issue_4') + expect(results_list.all('p')[1]).to have_content('issue_2') + expect(results_list.all('p')[2]).to have_content('issue_3') + expect(results_list.all('p')[3]).to have_content('issue_1') + expect(results_list.all('p')[4]).to have_content('merge_request_1') end - it "sorts by label priority" do - click_link "Label priority" + it 'sorts by label priority' do + click_link 'Label priority' results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("issue_3") - expect(results_list.all('p')[1]).to have_content("merge_request_1") - expect(results_list.all('p')[2]).to have_content("issue_1") - expect(results_list.all('p')[3]).to have_content("issue_2") - expect(results_list.all('p')[4]).to have_content("issue_4") + expect(results_list.all('p')[0]).to have_content('issue_3') + expect(results_list.all('p')[1]).to have_content('merge_request_1') + expect(results_list.all('p')[2]).to have_content('issue_1') + expect(results_list.all('p')[3]).to have_content('issue_2') + expect(results_list.all('p')[4]).to have_content('issue_4') end end @@ -81,17 +83,17 @@ describe "Dashboard > User sorts todos", feature: true do create(:todo, user: user, project: project, target: issue_2) create(:todo, user: user, project: project, target: merge_request_1) - login_as(user) + gitlab_sign_in(user) visit dashboard_todos_path end it "doesn't mix issues and merge requests label priorities" do - click_link "Label priority" + click_link 'Label priority' results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("issue_1") - expect(results_list.all('p')[1]).to have_content("issue_2") - expect(results_list.all('p')[2]).to have_content("merge_request_1") + expect(results_list.all('p')[0]).to have_content('issue_1') + expect(results_list.all('p')[1]).to have_content('issue_2') + expect(results_list.all('p')[2]).to have_content('merge_request_1') end end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb new file mode 100644 index 00000000000..24da5db305f --- /dev/null +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -0,0 +1,355 @@ +require 'spec_helper' + +feature 'Dashboard Todos' do + let(:user) { create(:user) } + let(:author) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, due_date: Date.today) } + + context 'User does not have todos' do + before do + sign_in(user) + visit dashboard_todos_path + end + + it 'shows "All done" message' do + expect(page).to have_content 'Todos let you see what you should do next.' + end + end + + context 'User has a todo', js: true do + before do + create(:todo, :mentioned, user: user, project: project, target: issue, author: author) + sign_in(user) + + visit dashboard_todos_path + end + + it 'has todo present' do + expect(page).to have_selector('.todos-list .todo', count: 1) + end + + it 'shows due date as today' do + within first('.todo') do + expect(page).to have_content 'Due today' + end + end + + shared_examples 'deleting the todo' do + before do + within first('.todo') do + click_link 'Done' + end + end + + it 'is marked as done-reversible in the list' do + expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible') + end + + it 'shows Undo button' do + expect(page).to have_selector('.js-undo-todo', visible: true) + expect(page).to have_selector('.js-done-todo', visible: false) + end + + it 'updates todo count' do + expect(page).to have_content 'To do 0' + expect(page).to have_content 'Done 1' + end + + it 'has not "All done" message' do + expect(page).not_to have_selector('.todos-all-done') + end + end + + shared_examples 'deleting and restoring the todo' do + before do + within first('.todo') do + click_link 'Done' + wait_for_requests + click_link 'Undo' + end + end + + it 'is marked back as pending in the list' do + expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible') + expect(page).to have_selector('.todos-list .todo.todo-pending') + end + + it 'shows Done button' do + expect(page).to have_selector('.js-undo-todo', visible: false) + expect(page).to have_selector('.js-done-todo', visible: true) + end + + it 'updates todo count' do + expect(page).to have_content 'To do 1' + expect(page).to have_content 'Done 0' + end + end + + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' + + context 'todo is stale on the page' do + before do + todos = TodosFinder.new(user, state: :pending).execute + TodoService.new.mark_todos_as_done(todos, user) + end + + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' + end + end + + context 'User created todos for themself' do + before do + sign_in(user) + end + + context 'issue assigned todo' do + before do + create(:todo, :assigned, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows issue assigned to yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You assigned issue #{issue.to_reference(full: true)} to yourself") + end + end + end + + context 'marked todo' do + before do + create(:todo, :marked, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you added a todo message' do + page.within('.js-todos-all') do + expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'mentioned todo' do + before do + create(:todo, :mentioned, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you mentioned yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'directly_addressed todo' do + before do + create(:todo, :directly_addressed, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you directly addressed yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'approval todo' do + let(:merge_request) { create(:merge_request) } + + before do + create(:todo, :approval_required, user: user, project: project, target: merge_request, author: user) + visit dashboard_todos_path + end + + it 'shows you set yourself as an approver message' do + page.within('.js-todos-all') do + expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + end + + context 'User has done todos', js: true do + before do + create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author) + sign_in(user) + visit dashboard_todos_path(state: :done) + end + + it 'has the done todo present' do + expect(page).to have_selector('.todos-list .todo.todo-done', count: 1) + end + + describe 'restoring the todo' do + before do + within first('.todo') do + click_link 'Add todo' + end + end + + it 'is removed from the list' do + expect(page).not_to have_selector('.todos-list .todo.todo-done') + end + + it 'updates todo count' do + expect(page).to have_content 'To do 1' + expect(page).to have_content 'Done 0' + end + end + end + + context 'User has Todos with labels spanning multiple projects' do + before do + label1 = create(:label, project: project) + note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project) + create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id) + + project2 = create(:project, :public) + label2 = create(:label, project: project2) + issue2 = create(:issue, project: project2) + note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2) + create(:todo, :mentioned, project: project2, target: issue2, user: user, note_id: note2.id) + + gitlab_sign_in(user) + visit dashboard_todos_path + end + + it 'shows page with two Todos' do + expect(page).to have_selector('.todos-list .todo', count: 2) + end + end + + context 'User has multiple pages of Todos' do + before do + allow(Todo).to receive(:default_per_page).and_return(1) + + # Create just enough records to cause us to paginate + create_list(:todo, 2, :mentioned, user: user, project: project, target: issue, author: author) + + sign_in(user) + end + + it 'is paginated' do + visit dashboard_todos_path + + expect(page).to have_selector('.gl-pagination') + end + + it 'is has the right number of pages' do + visit dashboard_todos_path + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + + describe 'mark all as done', js: true do + before do + visit dashboard_todos_path + find('.js-todos-mark-all').trigger('click') + end + + it 'shows "All done" message!' do + expect(page).to have_content 'To do 0' + expect(page).to have_content "You're all done!" + expect(page).not_to have_selector('.gl-pagination') + end + + it 'shows "Undo mark all as done" button' do + expect(page).to have_selector('.js-todos-mark-all', visible: false) + expect(page).to have_selector('.js-todos-undo-all', visible: true) + end + end + + describe 'undo mark all as done', js: true do + before do + visit dashboard_todos_path + end + + it 'shows the restored todo list' do + mark_all_and_undo + + expect(page).to have_selector('.todos-list .todo', count: 1) + expect(page).to have_selector('.gl-pagination') + expect(page).not_to have_content "You're all done!" + end + + it 'updates todo count' do + mark_all_and_undo + + expect(page).to have_content 'To do 2' + expect(page).to have_content 'Done 0' + end + + it 'shows "Mark all as done" button' do + mark_all_and_undo + + expect(page).to have_selector('.js-todos-mark-all', visible: true) + expect(page).to have_selector('.js-todos-undo-all', visible: false) + end + + context 'User has deleted a todo' do + before do + within first('.todo') do + click_link 'Done' + end + end + + it 'shows the restored todo list with the deleted todo' do + mark_all_and_undo + + expect(page).to have_selector('.todos-list .todo.todo-pending', count: 1) + end + end + + def mark_all_and_undo + find('.js-todos-mark-all').trigger('click') + wait_for_requests + find('.js-todos-undo-all').trigger('click') + wait_for_requests + end + end + end + + context 'User has a Todo in a project pending deletion' do + before do + deleted_project = create(:project, :public, pending_delete: true) + create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) + create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author, state: :done) + sign_in(user) + visit dashboard_todos_path + end + + it 'shows "All done" message' do + within('.todos-count') { expect(page).to have_content '0' } + expect(page).to have_content 'To do 0' + expect(page).to have_content 'Done 0' + expect(page).to have_selector('.todos-all-done', count: 1) + end + end + + context 'User has a Build Failed todo' do + let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } + + before do + sign_in(user) + visit dashboard_todos_path + end + + it 'shows the todo' do + expect(page).to have_content 'The build failed for merge request' + end + + it 'links to the pipelines for the merge request' do + href = pipelines_namespace_project_merge_request_path(project.namespace, project, todo.target) + + expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href + end + end +end diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb index 34d6257f5fd..e9f34760143 100644 --- a/spec/features/dashboard/user_filters_projects_spec.rb +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -9,7 +9,7 @@ describe 'Dashboard > User filters projects', :feature do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end describe 'filtering personal projects' do diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index 1c53f6dff06..c4dbaad2895 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -8,7 +8,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do context 'filtering by milestone' do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) create(:issue, project: project, author: user, assignees: [user]) create(:issue, project: project, author: user, assignees: [user], milestone: milestone) diff --git a/spec/features/dashboard_milestones_spec.rb b/spec/features/dashboard_milestones_spec.rb index f32fddbc9fa..b308a2297b9 100644 --- a/spec/features/dashboard_milestones_spec.rb +++ b/spec/features/dashboard_milestones_spec.rb @@ -17,7 +17,7 @@ feature 'Dashboard > Milestones', feature: true do let!(:milestone) { create(:milestone, project: project) } before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) visit dashboard_milestones_path end diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb index 96e0b78f6b9..96128061e4d 100644 --- a/spec/features/discussion_comments/commit_spec.rb +++ b/spec/features/discussion_comments/commit_spec.rb @@ -9,7 +9,7 @@ describe 'Discussion Comments Merge Request', :feature, :js do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_commit_path(project.namespace, project, sample_commit.id) end diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb index ccc9efccd18..d7c1cd12fb5 100644 --- a/spec/features/discussion_comments/issue_spec.rb +++ b/spec/features/discussion_comments/issue_spec.rb @@ -7,7 +7,7 @@ describe 'Discussion Comments Issue', :feature, :js do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb index f99ebeb9cd9..31fb9c72d25 100644 --- a/spec/features/discussion_comments/merge_request_spec.rb +++ b/spec/features/discussion_comments/merge_request_spec.rb @@ -7,7 +7,7 @@ describe 'Discussion Comments Merge Request', :feature, :js do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb index 19a306511b2..998d633c83d 100644 --- a/spec/features/discussion_comments/snippets_spec.rb +++ b/spec/features/discussion_comments/snippets_spec.rb @@ -7,7 +7,7 @@ describe 'Discussion Comments Issue', :feature, :js do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_snippet_path(project.namespace, project, snippet) end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index c4d5077e5e1..ea749528c11 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -10,7 +10,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(100.kilobytes) allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(10.kilobytes) - login_as :admin + gitlab_sign_in :admin # Ensure that undiffable.md is in .gitattributes project.repository.copy_gitattributes(branch) @@ -140,7 +140,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do end context 'reloading the page' do - before { refresh } + before do + refresh + end it 'collapses the large diff by default' do expect(large_diff).not_to have_selector('.code') @@ -262,7 +264,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for elements to appear to ensure full page reload expect(page).to have_content('This diff was suppressed by a .gitattributes entry') - expect(page).to have_content('This diff could not be displayed because it is too large.') + expect(page).to have_content('This source diff could not be displayed because it is too large.') expect(page).to have_content('too_large_image.jpg') find('.note-textarea') diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index d4284ed099b..6be5dee0c3c 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -10,7 +10,7 @@ describe 'Explore Groups page', :js, :feature do before do group.add_owner(user) - login_as(user) + gitlab_sign_in(user) visit explore_groups_path end diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb index 15a6354211b..2d7e703688f 100644 --- a/spec/features/explore/new_menu_spec.rb +++ b/spec/features/explore/new_menu_spec.rb @@ -16,7 +16,7 @@ feature 'Top Plus Menu', feature: true, js: true do context 'used by full user' do before do - login_as(user) + gitlab_sign_in(user) end scenario 'click on New project shows new project page' do @@ -103,7 +103,7 @@ feature 'Top Plus Menu', feature: true, js: true do context 'used by guest user' do before do - login_as(guest_user) + gitlab_sign_in(guest_user) end scenario 'click on New issue shows new issue page' do diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 55092412340..2d13af2a52a 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe "GitLab Flavored Markdown", feature: true do + let(:user) { create(:user) } let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:fred) do @@ -10,8 +11,8 @@ describe "GitLab Flavored Markdown", feature: true do end before do - login_as(:user) - project.add_developer(@user) + sign_in(user) + project.add_developer(user) end describe "for commits" do @@ -19,8 +20,8 @@ describe "GitLab Flavored Markdown", feature: true do let(:commit) { project.commit } before do - allow_any_instance_of(Commit).to receive(:title). - and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details") + allow_any_instance_of(Commit).to receive(:title) + .and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details") end it "renders title in commits#index" do @@ -51,12 +52,12 @@ describe "GitLab Flavored Markdown", feature: true do describe "for issues", feature: true, js: true do before do @other_issue = create(:issue, - author: @user, - assignees: [@user], + author: user, + assignees: [user], project: project) @issue = create(:issue, - author: @user, - assignees: [@user], + author: user, + assignees: [user], project: project, title: "fix #{@other_issue.to_reference}", description: "ask #{fred.to_reference} for details") diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb index 4b22b07494d..54ebfe6cf77 100644 --- a/spec/features/global_search_spec.rb +++ b/spec/features/global_search_spec.rb @@ -6,7 +6,7 @@ feature 'Global search', feature: true do before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) end describe 'I search through the issues and I see pagination' do diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb index 81f9c103e95..9f66a3d8c72 100644 --- a/spec/features/groups/activity_spec.rb +++ b/spec/features/groups/activity_spec.rb @@ -7,7 +7,7 @@ feature 'Group activity page', feature: true do context 'when signed in' do before do user = create(:group_member, :developer, user: create(:user), group: group ).user - login_as(user) + gitlab_sign_in(user) visit path end diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb index fef8e41bffe..b1c7151dfa8 100644 --- a/spec/features/groups/empty_states_spec.rb +++ b/spec/features/groups/empty_states_spec.rb @@ -5,7 +5,7 @@ feature 'Groups Merge Requests Empty States' do let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user } before do - login_as(user) + gitlab_sign_in(user) end context 'group has a project' do diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb index dfc3c84f29a..f450626c370 100644 --- a/spec/features/groups/group_name_toggle_spec.rb +++ b/spec/features/groups/group_name_toggle_spec.rb @@ -9,7 +9,7 @@ feature 'Group name toggle', feature: true, js: true do SMALL_SCREEN = 300 before do - login_as :user + gitlab_sign_in :user end it 'is not present if enough horizontal space' do diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index cc25db4ad60..56e163ec4d0 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -6,7 +6,7 @@ feature 'Edit group settings', feature: true do background do group.add_owner(user) - login_as(user) + gitlab_sign_in(user) end describe 'when the group path is changed' do @@ -18,14 +18,14 @@ feature 'Edit group settings', feature: true do update_path(new_group_path) visit new_group_full_path expect(current_path).to eq(new_group_full_path) - expect(find('h1.group-title')).to have_content(new_group_path) + expect(find('h1.group-title')).to have_content(group.name) end scenario 'the old group path redirects to the new path' do update_path(new_group_path) visit old_group_full_path expect(current_path).to eq(new_group_full_path) - expect(find('h1.group-title')).to have_content(new_group_path) + expect(find('h1.group-title')).to have_content(group.name) end context 'with a subgroup' do @@ -37,14 +37,14 @@ feature 'Edit group settings', feature: true do update_path(new_group_path) visit new_subgroup_full_path expect(current_path).to eq(new_subgroup_full_path) - expect(find('h1.group-title')).to have_content(subgroup.path) + expect(find('h1.group-title')).to have_content(subgroup.name) end scenario 'the old subgroup path redirects to the new path' do update_path(new_group_path) visit old_subgroup_full_path expect(current_path).to eq(new_subgroup_full_path) - expect(find('h1.group-title')).to have_content(subgroup.path) + expect(find('h1.group-title')).to have_content(subgroup.name) end end @@ -52,9 +52,14 @@ feature 'Edit group settings', feature: true do given!(:project) { create(:project, group: group, path: 'project') } given(:old_project_full_path) { "/#{group.path}/#{project.path}" } given(:new_project_full_path) { "/#{new_group_path}/#{project.path}" } - - before(:context) { TestEnv.clean_test_path } - after(:example) { TestEnv.clean_test_path } + + before(:context) do + TestEnv.clean_test_path + end + + after(:example) do + TestEnv.clean_test_path + end scenario 'the project is accessible via the new path' do update_path(new_group_path) diff --git a/spec/features/groups/labels/edit_spec.rb b/spec/features/groups/labels/edit_spec.rb index 69281cecb7b..b33040ef843 100644 --- a/spec/features/groups/labels/edit_spec.rb +++ b/spec/features/groups/labels/edit_spec.rb @@ -7,7 +7,7 @@ feature 'Edit group label', feature: true do background do group.add_owner(user) - login_as(user) + gitlab_sign_in(user) visit edit_group_label_path(group, label) end diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb new file mode 100644 index 00000000000..8b891c52d08 --- /dev/null +++ b/spec/features/groups/labels/subscription_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +feature 'Labels subscription', feature: true do + let(:user) { create(:user) } + let(:group) { create(:group) } + let!(:feature) { create(:group_label, group: group, title: 'feature') } + + context 'when signed in' do + before do + group.add_developer(user) + gitlab_sign_in user + end + + scenario 'users can subscribe/unsubscribe to group labels', js: true do + visit group_labels_path(group) + + expect(page).to have_content('feature') + + within "#group_label_#{feature.id}" do + expect(page).not_to have_button 'Unsubscribe' + + click_button 'Subscribe' + + expect(page).not_to have_button 'Subscribe' + expect(page).to have_button 'Unsubscribe' + + click_button 'Unsubscribe' + + expect(page).to have_button 'Subscribe' + expect(page).not_to have_button 'Unsubscribe' + end + end + end + + context 'when not signed in' do + it 'users can not subscribe/unsubscribe to labels' do + visit group_labels_path(group) + + expect(page).to have_content 'feature' + expect(page).not_to have_button('Subscribe') + end + end + + def click_link_on_dropdown(text) + find('.dropdown-group-label').click + + page.within('.dropdown-group-label') do + find('a.js-subscribe-button', text: text).click + end + end +end diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb deleted file mode 100644 index be60b0489c7..00000000000 --- a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -feature 'Groups > Members > Last owner cannot leave group', feature: true do - let(:owner) { create(:user) } - let(:group) { create(:group) } - - background do - group.add_owner(owner) - login_as(owner) - visit group_path(group) - end - - scenario 'user does not see a "Leave group" link' do - expect(page).not_to have_content 'Leave group' - end -end diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb new file mode 100644 index 00000000000..b438f57753c --- /dev/null +++ b/spec/features/groups/members/leave_group_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +feature 'Groups > Members > Leave group', feature: true do + let(:user) { create(:user) } + let(:other_user) { create(:user) } + let(:group) { create(:group) } + + background do + gitlab_sign_in(user) + end + + scenario 'guest leaves the group' do + group.add_guest(user) + group.add_owner(other_user) + + visit group_path(group) + click_link 'Leave group' + + expect(current_path).to eq(dashboard_groups_path) + expect(page).to have_content left_group_message(group) + expect(group.users).not_to include(user) + end + + scenario 'guest leaves the group as last member' do + group.add_guest(user) + + visit group_path(group) + click_link 'Leave group' + + expect(current_path).to eq(dashboard_groups_path) + expect(page).to have_content left_group_message(group) + expect(group.users).not_to include(user) + end + + scenario 'owner leaves the group if they is not the last owner' do + group.add_owner(user) + group.add_owner(other_user) + + visit group_path(group) + click_link 'Leave group' + + expect(current_path).to eq(dashboard_groups_path) + expect(page).to have_content left_group_message(group) + expect(group.users).not_to include(user) + end + + scenario 'owner can not leave the group if they is a last owner' do + group.add_owner(user) + + visit group_path(group) + + expect(page).not_to have_content 'Leave group' + + visit group_group_members_path(group) + + expect(find(:css, '.project-members-page li', text: user.name)).not_to have_selector(:css, 'a.btn-remove') + end + + def left_group_message(group) + "You left the \"#{group.name}\"" + end +end diff --git a/spec/features/groups/members/list_members_spec.rb b/spec/features/groups/members/list_members_spec.rb new file mode 100644 index 00000000000..f6493c4c50e --- /dev/null +++ b/spec/features/groups/members/list_members_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +feature 'Groups > Members > List members', feature: true do + include Select2Helper + + let(:user1) { create(:user, name: 'John Doe') } + let(:user2) { create(:user, name: 'Mary Jane') } + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + background do + gitlab_sign_in(user1) + end + + scenario 'show members from current group and parent', :nested_groups do + group.add_developer(user1) + nested_group.add_developer(user2) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row.text).to include(user2.name) + end + + scenario 'show user once if member of both current group and parent', :nested_groups do + group.add_developer(user1) + nested_group.add_developer(user1) + + visit group_group_members_path(nested_group) + + expect(first_row.text).to include(user1.name) + expect(second_row).to be_blank + end + + def first_row + page.all('ul.content-list > li')[0] + end + + def second_row + page.all('ul.content-list > li')[1] + end +end diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/manage_access_requests_spec.rb index dbe150823ba..f84d8594c65 100644 --- a/spec/features/groups/members/owner_manages_access_requests_spec.rb +++ b/spec/features/groups/members/manage_access_requests_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Groups > Members > Owner manages access requests', feature: true do +feature 'Groups > Members > Manage access requests', feature: true do let(:user) { create(:user) } let(:owner) { create(:user) } let(:group) { create(:group, :public, :access_requestable) } @@ -8,7 +8,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do background do group.request_access(user) group.add_owner(owner) - login_as(owner) + gitlab_sign_in(owner) end scenario 'owner can see access requests' do @@ -17,7 +17,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do expect_visible_access_request(group, user) end - scenario 'master can grant access' do + scenario 'owner can grant access' do visit group_group_members_path(group) expect_visible_access_request(group, user) @@ -28,7 +28,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted" end - scenario 'master can deny access' do + scenario 'owner can deny access' do visit group_group_members_path(group) expect_visible_access_request(group, user) diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/manage_members.rb index f654fa16a06..a9a654b20e2 100644 --- a/spec/features/groups/members/list_spec.rb +++ b/spec/features/groups/members/manage_members.rb @@ -1,35 +1,14 @@ require 'spec_helper' -feature 'Groups members list', feature: true do +feature 'Groups > Members > Manage members', feature: true do include Select2Helper let(:user1) { create(:user, name: 'John Doe') } let(:user2) { create(:user, name: 'Mary Jane') } let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } background do - login_as(user1) - end - - scenario 'show members from current group and parent', :nested_groups do - group.add_developer(user1) - nested_group.add_developer(user2) - - visit group_group_members_path(nested_group) - - expect(first_row.text).to include(user1.name) - expect(second_row.text).to include(user2.name) - end - - scenario 'show user once if member of both current group and parent', :nested_groups do - group.add_developer(user1) - nested_group.add_developer(user1) - - visit group_group_members_path(nested_group) - - expect(first_row.text).to include(user1.name) - expect(second_row).to be_blank + gitlab_sign_in(user1) end scenario 'update user to owner level', :js do @@ -59,6 +38,18 @@ feature 'Groups members list', feature: true do end end + scenario 'remove user from group', :js do + group.add_owner(user1) + group.add_developer(user2) + + visit group_group_members_path(group) + + find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click + + expect(page).not_to have_content(user2.name) + expect(group.users).not_to include(user2) + end + scenario 'add yourself to group when already an owner', :js do group.add_owner(user1) @@ -86,6 +77,23 @@ feature 'Groups members list', feature: true do end end + scenario 'guest can not manage other users' do + group.add_guest(user1) + group.add_developer(user2) + + visit group_group_members_path(group) + + expect(page).not_to have_button 'Add to group' + + page.within(second_row) do + # Can not modify user2 role + expect(page).not_to have_button 'Developer' + + # Can not remove user2 + expect(page).not_to have_css('a.btn-remove') + end + end + def first_row page.all('ul.content-list > li')[0] end diff --git a/spec/features/groups/members/member_cannot_request_access_to_his_project_spec.rb b/spec/features/groups/members/member_cannot_request_access_to_his_project_spec.rb deleted file mode 100644 index 37c433cc09a..00000000000 --- a/spec/features/groups/members/member_cannot_request_access_to_his_project_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -feature 'Groups > Members > Member cannot request access to his project', feature: true do - let(:member) { create(:user) } - let(:group) { create(:group) } - - background do - group.add_developer(member) - login_as(member) - visit group_path(group) - end - - scenario 'member does not see the request access button' do - expect(page).not_to have_content 'Request Access' - end -end diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb deleted file mode 100644 index ac4d94658ae..00000000000 --- a/spec/features/groups/members/member_leaves_group_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -feature 'Groups > Members > Member leaves group', feature: true do - let(:user) { create(:user) } - let(:owner) { create(:user) } - let(:group) { create(:group, :public) } - - background do - group.add_owner(owner) - group.add_developer(user) - login_as(user) - visit group_path(group) - end - - scenario 'user leaves group' do - click_link 'Leave group' - - expect(current_path).to eq(dashboard_groups_path) - expect(group.users.exists?(user.id)).to be_falsey - end -end diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/request_access_spec.rb index e4b5ea91bd3..41c31b62e18 100644 --- a/spec/features/groups/members/user_requests_access_spec.rb +++ b/spec/features/groups/members/request_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Groups > Members > User requests access', feature: true do +feature 'Groups > Members > Request access', feature: true do let(:user) { create(:user) } let(:owner) { create(:user) } let(:group) { create(:group, :public, :access_requestable) } @@ -8,7 +8,7 @@ feature 'Groups > Members > User requests access', feature: true do background do group.add_owner(owner) - login_as(user) + gitlab_sign_in(user) visit group_path(group) end @@ -68,4 +68,11 @@ feature 'Groups > Members > User requests access', feature: true do expect(group.requesters.exists?(user_id: user)).to be_falsey expect(page).to have_content 'Your access request to the group has been withdrawn.' end + + scenario 'member does not see the request access button' do + group.add_owner(user) + visit group_path(group) + + expect(page).not_to have_content 'Request Access' + end end diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sort_members_spec.rb index 902d3f789ff..8ee61953844 100644 --- a/spec/features/groups/members/sorting_spec.rb +++ b/spec/features/groups/members/sort_members_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Groups > Members > Sorting', feature: true do +feature 'Groups > Members > Sort members', feature: true do let(:owner) { create(:user, name: 'John Doe') } let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } let(:group) { create(:group) } @@ -9,7 +9,7 @@ feature 'Groups > Members > Sorting', feature: true do create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago) create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago) - login_as(owner) + gitlab_sign_in(owner) end scenario 'sorts alphabetically by default' do diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index daa2c6afd63..330310eae6b 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -8,7 +8,7 @@ feature 'Group milestones', :feature, :js do before do Timecop.freeze - login_as(user) + gitlab_sign_in(user) end after do diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index d3c49c37374..76575f61528 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -7,7 +7,7 @@ feature 'Group show page', feature: true do context 'when signed in' do before do user = create(:group_member, :developer, user: create(:user), group: group ).user - login_as(user) + gitlab_sign_in(user) visit path end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 24ea7aba0cc..ecacca00a61 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' feature 'Group', feature: true do before do - login_as(:admin) + gitlab_sign_in(:admin) end matcher :have_namespace_error_message do @@ -12,7 +12,9 @@ feature 'Group', feature: true do end describe 'create a group' do - before { visit new_group_path } + before do + visit new_group_path + end describe 'with space in group path' do it 'renders new group form with validation errors' do @@ -106,8 +108,8 @@ feature 'Group', feature: true do before do group.add_owner(user) - logout - login_as(user) + gitlab_sign_out + gitlab_sign_in(user) visit subgroups_group_path(group) click_link 'New Subgroup' @@ -126,8 +128,8 @@ feature 'Group', feature: true do it 'checks permissions to avoid exposing groups by parent_id' do group = create(:group, :private, path: 'secret-group') - logout - login_as(:user) + gitlab_sign_out + gitlab_sign_in(:user) visit new_group_path(parent_id: group.id) expect(page).not_to have_content('secret-group') @@ -138,7 +140,9 @@ feature 'Group', feature: true do let(:path) { edit_group_path(group) } let(:new_name) { 'new-name' } - before { visit path } + before do + visit path + end it 'saves new settings' do fill_in 'group_name', with: new_name diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 31014f5cad2..b01ee1cf491 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -37,10 +37,10 @@ describe 'Help Pages', feature: true do context 'in a production environment with version check enabled', :js do before do allow(Rails.env).to receive(:production?) { true } - allow(current_application_settings).to receive(:version_check_enabled) { true } + allow_any_instance_of(ApplicationSetting).to receive(:version_check_enabled) { true } allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' } - login_as :user + gitlab_sign_in :user visit help_path end @@ -53,4 +53,27 @@ describe 'Help Pages', feature: true do expect(find('.js-version-status-badge', visible: false)).not_to be_visible end end + + describe 'when help page is customized' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:help_page_hide_commercial_content?) { true } + allow_any_instance_of(ApplicationSetting).to receive(:help_page_text) { "My Custom Text" } + allow_any_instance_of(ApplicationSetting).to receive(:help_page_support_url) { "http://example.com/help" } + + gitlab_sign_in(:user) + visit help_path + end + + it 'should display custom help page text' do + expect(page).to have_text "My Custom Text" + end + + it 'should hide marketing content when enabled' do + expect(page).not_to have_link "Get a support subscription" + end + + it 'should use a custom support url' do + expect(page).to have_link "See our website for getting help", href: "http://example.com/help" + end + end end diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index bfe43bff10f..56c9b10e757 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -153,7 +153,9 @@ describe 'Projects > Issuables > Default sort order', feature: true do context 'when the sort in the URL is id_desc' do let(:issuable_type) { :issue } - before { visit_issues(project, sort: 'id_desc') } + before do + visit_issues(project, sort: 'id_desc') + end it 'shows the sort order as last created' do expect(find('.issues-other-filters')).to have_content('Last created') @@ -165,7 +167,9 @@ describe 'Projects > Issuables > Default sort order', feature: true do context 'when the sort in the URL is id_asc' do let(:issuable_type) { :issue } - before { visit_issues(project, sort: 'id_asc') } + before do + visit_issues(project, sort: 'id_asc') + end it 'shows the sort order as oldest created' do expect(find('.issues-other-filters')).to have_content('Oldest created') diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index 414838fa22e..f3a5a8463d1 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -8,7 +8,7 @@ describe 'issuable list', feature: true do before do project.add_user(user, :developer) - login_as(user) + gitlab_sign_in(user) issuable_types.each { |type| create_issuables(type) } end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 81ae54c7a10..6698e2c79a1 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -12,7 +12,7 @@ describe 'Awards Emoji', feature: true do context 'authorized user' do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end describe 'visiting an issue with a legacy award emoji that is not valid anymore' do @@ -81,13 +81,13 @@ describe 'Awards Emoji', feature: true do end end - context 'execute /award slash command' do + context 'execute /award quick action' do it 'toggles the emoji award on noteable', js: true do - execute_slash_command('/award :100:') + execute_quick_action('/award :100:') expect(find(noteable_award_counter)).to have_text("1") - execute_slash_command('/award :100:') + execute_quick_action('/award :100:') expect(page).not_to have_selector(noteable_award_counter) end @@ -105,7 +105,7 @@ describe 'Awards Emoji', feature: true do end end - def execute_slash_command(cmd) + def execute_quick_action(cmd) within('.js-main-target-form') do fill_in 'note[note]', with: cmd click_button 'Comment' diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb index fcf22dd5033..a1c97caea20 100644 --- a/spec/features/issues/award_spec.rb +++ b/spec/features/issues/award_spec.rb @@ -7,7 +7,7 @@ feature 'Issue awards', js: true, feature: true do describe 'logged in' do before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) wait_for_requests end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 95b4930cd32..a99c19cb787 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -13,7 +13,22 @@ feature 'Issues > Labels bulk assignment', feature: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user + end + + context 'sidebar' do + before do + enable_bulk_update + end + + it 'is present when bulk edit is enabled' do + expect(page).to have_css('.issuable-sidebar') + end + + it 'is not present when bulk edit is disabled' do + disable_bulk_update + expect(page).not_to have_css('.issuable-sidebar') + end end context 'can bulk assign' do @@ -331,7 +346,7 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'as a guest' do before do - login_as user + gitlab_sign_in user visit namespace_project_issues_path(project.namespace, project) end @@ -398,4 +413,8 @@ feature 'Issues > Labels bulk assignment', feature: true do visit namespace_project_issues_path(project.namespace, project) click_button 'Edit Issues' end + + def disable_bulk_update + click_button 'Cancel' + end end diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb index 1d7d8d291b2..aa538803dd8 100644 --- a/spec/features/issues/create_branch_merge_request_spec.rb +++ b/spec/features/issues/create_branch_merge_request_spec.rb @@ -8,7 +8,7 @@ feature 'Create Branch/Merge Request Dropdown on issue page', feature: true, js: context 'for team members' do before do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) end it 'allows creating a merge request from the issue page' do diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb index 24e2419b5ce..5f631043e15 100644 --- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -9,7 +9,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu describe 'as a user with access to the project' do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -82,7 +82,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu describe 'as a reporter' do before do project.team << [user, :reporter] - login_as user + gitlab_sign_in user visit new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid) end diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb index 3a5a79e03f4..9e9e214060f 100644 --- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb @@ -9,7 +9,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue', fe describe 'As a user with access to the project' do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -66,7 +66,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue', fe describe 'as a reporter' do before do project.team << [user, :reporter] - login_as user + gitlab_sign_in user visit new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 44353d880c2..96f6739af2d 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -23,7 +23,7 @@ describe 'Dropdown assignee', :feature, :js do project.team << [user, :master] project.team << [user_john, :master] project.team << [user_jacob, :master] - login_as(user) + gitlab_sign_in(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 6b707c4be4a..5ee824c662a 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -31,7 +31,7 @@ describe 'Dropdown author', js: true, feature: true do project.team << [user, :master] project.team << [user_john, :master] project.team << [user_jacob, :master] - login_as(user) + gitlab_sign_in(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index b9a37cfcc22..a05e4394ffd 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -14,7 +14,7 @@ describe 'Dropdown hint', :js, :feature do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index abe5d61e38c..aec9d7ceb5d 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -34,7 +34,7 @@ describe 'Dropdown label', js: true, feature: true do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 448259057b0..b21f41946b7 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -30,7 +30,7 @@ describe 'Dropdown milestone', :feature, :js do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 3ea95aed0a6..806c732b935 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -9,7 +9,7 @@ describe 'Search bar', js: true, feature: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index ff32b0c7d11..22488f34813 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -25,7 +25,7 @@ describe 'Visual tokens', js: true, feature: true do before do project.add_user(user, :master) project.add_user(user_rock, :master) - login_as(user) + gitlab_sign_in(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 96d37e33f3d..b369ef1ff79 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -16,7 +16,7 @@ describe 'New/edit issue', :feature, :js do before do project.team << [user, :master] project.team << [user2, :master] - login_as(user) + gitlab_sign_in(user) end context 'new issue' do @@ -210,6 +210,13 @@ describe 'New/edit issue', :feature, :js do expect(find('.js-assignee-search')).to have_content(user2.name) end + + it 'description has autocomplete' do + find('#issue_description').native.send_keys('') + fill_in 'issue_description', with: '@' + + expect(page).to have_selector('.atwho-view') + end end context 'edit issue' do @@ -258,6 +265,13 @@ describe 'New/edit issue', :feature, :js do end end end + + it 'description has autocomplete' do + find('#issue_description').native.send_keys('') + fill_in 'issue_description', with: '@' + + expect(page).to have_selector('.atwho-view') + end end describe 'sub-group project' do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 350473437a8..e61eb5233d0 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -8,7 +8,7 @@ feature 'GFM autocomplete', feature: true, js: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) wait_for_requests @@ -208,7 +208,7 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).not_to have_selector('.atwho-view') end - it 'triggers autocomplete after selecting a slash command' do + it 'triggers autocomplete after selecting a quick action' do note = find('#note_note') page.within '.timeline-content-form' do note.native.send_keys('') diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 96c24750250..163bc4bb32f 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -10,7 +10,7 @@ feature 'Issue Sidebar', feature: true do let!(:label) { create(:label, project: project, title: 'bug') } before do - login_as(user) + gitlab_sign_in(user) end context 'assignee', js: true do diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb index c8c9c50396b..66d823ec9d0 100644 --- a/spec/features/issues/markdown_toolbar_spec.rb +++ b/spec/features/issues/markdown_toolbar_spec.rb @@ -6,7 +6,7 @@ feature 'Issue markdown toolbar', feature: true, js: true do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index e75bf059218..21a7637fe7f 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -9,7 +9,7 @@ feature 'issue move to another project' do create(:issue, description: text, project: old_project, author: user) end - background { login_as(user) } + background { gitlab_sign_in(user) } context 'user does not have permission to move issue' do background do diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 2c0a6ffd3cb..bd31e44ef33 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -27,7 +27,7 @@ feature 'Issue notes polling', :feature, :js do let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) } before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end @@ -93,7 +93,7 @@ feature 'Issue notes polling', :feature, :js do let!(:existing_note) { create(:note, noteable: issue, project: project, author: user1, note: note_text) } before do - login_as(user2) + gitlab_sign_in(user2) visit namespace_project_issue_path(project.namespace, project, issue) end @@ -114,7 +114,7 @@ feature 'Issue notes polling', :feature, :js do let!(:system_note) { create(:system_note, noteable: issue, project: project, author: user, note: note_text) } before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb index 15c817cabac..f648295416f 100644 --- a/spec/features/issues/notes_on_issues_spec.rb +++ b/spec/features/issues/notes_on_issues_spec.rb @@ -9,7 +9,7 @@ describe 'Create notes on issues', :js, :feature do before do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) fill_in 'note[note]', with: note_text diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb index 6001476d0ca..57c783790b5 100644 --- a/spec/features/issues/spam_issues_spec.rb +++ b/spec/features/issues/spam_issues_spec.rb @@ -18,7 +18,7 @@ describe 'New issue', feature: true, js: true do ) project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'when identified as a spam' do diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 3fde85b0a5c..a1c00dd64f6 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -7,7 +7,7 @@ feature 'Manually create a todo item from issue', feature: true, js: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 8595847d313..dc981406e4e 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -7,7 +7,7 @@ feature 'Multiple issue updating from issues#index', feature: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'status', js: true do diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index d14c319707c..168cdd08137 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' -feature 'Issues > User uses slash commands', feature: true, js: true do - include SlashCommandsHelpers +feature 'Issues > User uses quick actions', feature: true, js: true do + include QuickActionsHelpers - it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do + it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do let(:issuable) { create(:issue, project: project) } end @@ -13,7 +13,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end @@ -41,8 +41,8 @@ feature 'Issues > User uses slash commands', feature: true, js: true do let(:guest) { create(:user) } before do project.team << [guest, :guest] - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit namespace_project_issue_path(project.namespace, project, issue) end @@ -81,8 +81,8 @@ feature 'Issues > User uses slash commands', feature: true, js: true do let(:guest) { create(:user) } before do project.team << [guest, :guest] - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index eecc565d2bd..f47b89fd718 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -5,20 +5,21 @@ describe 'Issues', feature: true do include IssueHelpers include SortingHelper + let(:user) { create(:user) } let(:project) { create(:empty_project, :public) } before do - login_as :user + sign_in(user) user2 = create(:user) - project.team << [[@user, user2], :developer] + project.team << [[user, user2], :developer] end describe 'Edit issue' do let!(:issue) do create(:issue, - author: @user, - assignees: [@user], + author: user, + assignees: [user], project: project) end @@ -35,15 +36,15 @@ describe 'Issues', feature: true do describe 'Editing issue assignee' do let!(:issue) do create(:issue, - author: @user, - assignees: [@user], + author: user, + assignees: [user], project: project) end it 'allows user to select unassigned', js: true do visit edit_namespace_project_issue_path(project.namespace, project, issue) - expect(page).to have_content "Assignee #{@user.name}" + expect(page).to have_content "Assignee #{user.name}" first('.js-user-search').click click_link 'Unassigned' @@ -86,7 +87,7 @@ describe 'Issues', feature: true do end context 'on edit form' do - let(:issue) { create(:issue, author: @user, project: project, due_date: Date.today.at_beginning_of_month.to_s) } + let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) } before do visit edit_namespace_project_issue_path(project.namespace, project, issue) @@ -131,10 +132,10 @@ describe 'Issues', feature: true do describe 'Issue info' do it 'excludes award_emoji from comment count' do - issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'foobar') + issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar') create(:award_emoji, awardable: issue) - visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) + visit namespace_project_issues_path(project.namespace, project, assignee_id: user.id) expect(page).to have_content 'foobar' expect(page.all('.no-comments').first.text).to eq "0" @@ -145,8 +146,8 @@ describe 'Issues', feature: true do before do %w(foobar barbaz gitlab).each do |title| create(:issue, - author: @user, - assignees: [@user], + author: user, + assignees: [user], project: project, title: title) end @@ -168,7 +169,7 @@ describe 'Issues', feature: true do end it 'allows filtering by a specified assignee' do - visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) + visit namespace_project_issues_path(project.namespace, project, assignee_id: user.id) expect(page).not_to have_content 'foobar' expect(page).to have_content 'barbaz' @@ -246,7 +247,10 @@ describe 'Issues', feature: true do context 'with a filter on labels' do let(:label) { create(:label, project: project) } - before { create(:label_link, label: label, target: foo) } + + before do + create(:label_link, label: label, target: foo) + end it 'sorts by least recently due date by excluding nil due dates' do bar.update(due_date: nil) @@ -363,13 +367,13 @@ describe 'Issues', feature: true do end describe 'when I want to reset my incoming email token' do - let(:project1) { create(:empty_project, namespace: @user.namespace) } + let(:project1) { create(:empty_project, namespace: user.namespace) } let!(:issue) { create(:issue, project: project1) } before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") - project1.team << [@user, :master] - visit namespace_project_issues_path(@user.namespace, project1) + project1.team << [user, :master] + visit namespace_project_issues_path(user.namespace, project1) end it 'changes incoming email address token', js: true do @@ -380,7 +384,7 @@ describe 'Issues', feature: true do wait_for_requests expect(page).to have_no_field('issue_email', with: previous_token) - new_token = project1.new_issue_address(@user.reload) + new_token = project1.new_issue_address(user.reload) expect(page).to have_field( 'issue_email', with: new_token @@ -389,7 +393,7 @@ describe 'Issues', feature: true do end describe 'update labels from issue#show', js: true do - let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) } + let(:issue) { create(:issue, project: project, author: user, assignees: [user]) } let!(:label) { create(:label, project: project) } before do @@ -408,14 +412,14 @@ describe 'Issues', feature: true do end describe 'update assignee from issue#show' do - let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) } + let(:issue) { create(:issue, project: project, author: user, assignees: [user]) } context 'by authorized user' do it 'allows user to select unassigned', js: true do visit namespace_project_issue_path(project.namespace, project, issue) page.within('.assignee') do - expect(page).to have_content "#{@user.name}" + expect(page).to have_content "#{user.name}" click_link 'Edit' click_link 'Unassigned' @@ -430,7 +434,7 @@ describe 'Issues', feature: true do end it 'allows user to select an assignee', js: true do - issue2 = create(:issue, project: project, author: @user) + issue2 = create(:issue, project: project, author: user) visit namespace_project_issue_path(project.namespace, project, issue2) page.within('.assignee') do @@ -442,28 +446,28 @@ describe 'Issues', feature: true do end page.within '.dropdown-menu-user' do - click_link @user.name + click_link user.name end page.within('.assignee') do - expect(page).to have_content @user.name + expect(page).to have_content user.name end end it 'allows user to unselect themselves', js: true do - issue2 = create(:issue, project: project, author: @user) + issue2 = create(:issue, project: project, author: user) visit namespace_project_issue_path(project.namespace, project, issue2) page.within '.assignee' do click_link 'Edit' - click_link @user.name + click_link user.name page.within '.value .author' do - expect(page).to have_content @user.name + expect(page).to have_content user.name end click_link 'Edit' - click_link @user.name + click_link user.name page.within '.value .assign-yourself' do expect(page).to have_content "No assignee" @@ -480,8 +484,8 @@ describe 'Issues', feature: true do end it 'shows assignee text', js: true do - logout - login_with guest + sign_out(:user) + sign_in(guest) visit namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content issue.assignees.first.name @@ -490,7 +494,7 @@ describe 'Issues', feature: true do end describe 'update milestone from issue#show' do - let!(:issue) { create(:issue, project: project, author: @user) } + let!(:issue) { create(:issue, project: project, author: user) } let!(:milestone) { create(:milestone, project: project) } context 'by authorized user' do @@ -543,8 +547,8 @@ describe 'Issues', feature: true do end it 'shows milestone text', js: true do - logout - login_with guest + sign_out(:user) + sign_in(guest) visit namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content milestone.title @@ -557,7 +561,7 @@ describe 'Issues', feature: true do context 'by unauthenticated user' do before do - logout + sign_out(:user) end it 'redirects to signin then back to new issue after signin' do @@ -567,7 +571,9 @@ describe 'Issues', feature: true do expect(current_path).to eq new_user_session_path - login_as :user + # NOTE: This is specifically testing the redirect after login, so we + # need the full login flow + gitlab_sign_in(create(:user)) expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project) end @@ -596,7 +602,7 @@ describe 'Issues', feature: true do before do project.repository.create_file( - @user, + user, '.gitlab/issue_templates/bug.md', 'this is a test "bug" template', message: 'added issue template', @@ -625,7 +631,7 @@ describe 'Issues', feature: true do it 'click the button to show modal for the new email' do page.within '#issue-email-modal' do - email = project.new_issue_address(@user) + email = project.new_issue_address(user) expect(page).to have_selector("input[value='#{email}']") end @@ -633,7 +639,7 @@ describe 'Issues', feature: true do end context 'with existing issues' do - let!(:issue) { create(:issue, project: project, author: @user) } + let!(:issue) { create(:issue, project: project, author: user) } it_behaves_like 'show the email in the modal' end @@ -645,7 +651,7 @@ describe 'Issues', feature: true do describe 'due date' do context 'update due on issue#show', js: true do - let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) } + let(:issue) { create(:issue, project: project, author: user, assignees: [user]) } before do visit namespace_project_issue_path(project.namespace, project, issue) @@ -690,7 +696,7 @@ describe 'Issues', feature: true do describe 'title issue#show', js: true do it 'updates the title', js: true do - issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'new title') + issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title') visit namespace_project_issue_path(project.namespace, project, issue) diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index c82e8c03343..a8055b21cee 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -36,7 +36,7 @@ feature 'Login', feature: true do it 'prevents the user from logging in' do user = create(:user, :blocked) - login_with(user) + gitlab_sign_in(user) expect(page).to have_content('Your account has been blocked.') end @@ -44,19 +44,19 @@ feature 'Login', feature: true do it 'does not update Devise trackable attributes', :redis do user = create(:user, :blocked) - expect { login_with(user) }.not_to change { user.reload.sign_in_count } + expect { gitlab_sign_in(user) }.not_to change { user.reload.sign_in_count } end end describe 'with the ghost user' do it 'disallows login' do - login_with(User.ghost) + gitlab_sign_in(User.ghost) expect(page).to have_content('Invalid Login or password.') end it 'does not update Devise trackable attributes', :redis do - expect { login_with(User.ghost) }.not_to change { User.ghost.reload.sign_in_count } + expect { gitlab_sign_in(User.ghost) }.not_to change { User.ghost.reload.sign_in_count } end end @@ -70,7 +70,7 @@ feature 'Login', feature: true do let(:user) { create(:user, :two_factor) } before do - login_with(user, remember: true) + gitlab_sign_in(user, remember: true) expect(page).to have_content('Two-Factor Authentication') end @@ -122,8 +122,8 @@ feature 'Login', feature: true do end it 'invalidates the used code' do - expect { enter_code(codes.sample) }. - to change { user.reload.otp_backup_codes.size }.by(-1) + expect { enter_code(codes.sample) } + .to change { user.reload.otp_backup_codes.size }.by(-1) end end @@ -143,31 +143,10 @@ feature 'Login', feature: true do end context 'logging in via OAuth' do - def saml_config - OpenStruct.new(name: 'saml', label: 'saml', args: { - assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback', - idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52', - idp_sso_target_url: 'https://idp.example.com/sso/saml', - issuer: 'https://localhost:3443/', - name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' - }) - end - - def stub_omniauth_config(messages) - Rails.application.env_config['devise.mapping'] = Devise.mappings[:user] - Rails.application.routes.disable_clear_and_finalize = true - Rails.application.routes.draw do - post '/users/auth/saml' => 'omniauth_callbacks#saml' - end - allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config) - allow(Gitlab.config.omniauth).to receive_messages(messages) - expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') - end - it 'shows 2FA prompt after OAuth login' do - stub_omniauth_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config]) + stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config]) user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') - login_via('saml', user, 'my-uid') + gitlab_sign_in_via('saml', user, 'my-uid') expect(page).to have_content('Two-Factor Authentication') enter_code(user.current_otp) @@ -180,19 +159,19 @@ feature 'Login', feature: true do let(:user) { create(:user) } it 'allows basic login' do - login_with(user) + gitlab_sign_in(user) expect(current_path).to eq root_path end it 'does not show a "You are already signed in." error message' do - login_with(user) + gitlab_sign_in(user) expect(page).not_to have_content('You are already signed in.') end it 'blocks invalid login' do user = create(:user, password: 'not-the-default') - login_with(user) + gitlab_sign_in(user) expect(page).to have_content('Invalid Login or password.') end end @@ -202,12 +181,14 @@ feature 'Login', feature: true do # TODO: otp_grace_period_started_at context 'global setting' do - before(:each) { stub_application_setting(require_two_factor_authentication: true) } + before do + stub_application_setting(require_two_factor_authentication: true) + end context 'with grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 48) - login_with(user) + gitlab_sign_in(user) end context 'within the grace period' do @@ -242,9 +223,9 @@ feature 'Login', feature: true do end context 'without grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 0) - login_with(user) + gitlab_sign_in(user) end it 'redirects to two-factor configuration page' do @@ -265,9 +246,9 @@ feature 'Login', feature: true do end context 'with grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 48) - login_with(user) + gitlab_sign_in(user) end context 'within the grace period' do @@ -306,9 +287,9 @@ feature 'Login', feature: true do end context 'without grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 0) - login_with(user) + gitlab_sign_in(user) end it 'redirects to two-factor configuration page' do diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index ba930de937d..534be3ab5a7 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -58,8 +58,8 @@ describe 'GitLab Markdown', feature: true do end it 'allows Markdown in tables' do - expect(doc.at_css('td:contains("Baz")').children.to_html). - to eq '<strong>Baz</strong>' + expect(doc.at_css('td:contains("Baz")').children.to_html) + .to eq '<strong>Baz</strong>' end it 'parses fenced code blocks' do @@ -158,14 +158,14 @@ describe 'GitLab Markdown', feature: true do describe 'Edge Cases' do it 'allows markup inside link elements' do aggregate_failures do - expect(doc.at_css('a[href="#link-emphasis"]').to_html). - to eq %{<a href="#link-emphasis"><em>text</em></a>} + expect(doc.at_css('a[href="#link-emphasis"]').to_html) + .to eq %{<a href="#link-emphasis"><em>text</em></a>} - expect(doc.at_css('a[href="#link-strong"]').to_html). - to eq %{<a href="#link-strong"><strong>text</strong></a>} + expect(doc.at_css('a[href="#link-strong"]').to_html) + .to eq %{<a href="#link-strong"><strong>text</strong></a>} - expect(doc.at_css('a[href="#link-code"]').to_html). - to eq %{<a href="#link-code"><code>text</code></a>} + expect(doc.at_css('a[href="#link-code"]').to_html) + .to eq %{<a href="#link-code"><code>text</code></a>} end end end diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb index b306e2f5f75..cb835f533e0 100644 --- a/spec/features/merge_requests/assign_issues_spec.rb +++ b/spec/features/merge_requests/assign_issues_spec.rb @@ -13,7 +13,7 @@ feature 'Merge request issue assignment', js: true, feature: true do end def visit_merge_request(current_user = nil) - login_as(current_user || user) + gitlab_sign_in(current_user || user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb index ac260e118d0..e9dd755b6af 100644 --- a/spec/features/merge_requests/award_spec.rb +++ b/spec/features/merge_requests/award_spec.rb @@ -7,7 +7,7 @@ feature 'Merge request awards', js: true, feature: true do describe 'logged in' do before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb index fa306c02a43..060cfb8fdd1 100644 --- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb +++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb @@ -6,7 +6,7 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) } before do - login_as user + gitlab_sign_in user project.team << [user, :master] end diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb index 6ba681e36f7..6ba96570e3d 100644 --- a/spec/features/merge_requests/cherry_pick_spec.rb +++ b/spec/features/merge_requests/cherry_pick_spec.rb @@ -7,7 +7,7 @@ describe 'Cherry-pick Merge Requests', js: true do let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) } before do - login_as user + gitlab_sign_in user project.team << [user, :master] end diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb index e627618042a..365b2555c35 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -20,7 +20,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_merge_request_path(project.namespace, project, merge_request) wait_for_requests diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 27e2d5d16f3..9c091befa27 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -79,20 +79,24 @@ feature 'Merge request conflict resolution', js: true, feature: true do context 'can be resolved in the UI' do before do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) end context 'the conflicts are resolvable' do let(:merge_request) { create_merge_request('conflict-resolvable') } - before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) } + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end it 'shows a link to the conflict resolution page' do expect(page).to have_link('conflicts', href: /\/conflicts\Z/) end context 'in Inline view mode' do - before { click_link('conflicts', href: /\/conflicts\Z/) } + before do + click_link('conflicts', href: /\/conflicts\Z/) + end include_examples "conflicts are resolved in Interactive mode" include_examples "conflicts are resolved in Edit inline mode" @@ -160,7 +164,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do before do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 82987c768d1..8f7adbccaaa 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -7,7 +7,7 @@ feature 'Create New Merge Request', feature: true, js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user end it 'selects the source branch sha when a tag with the same name exists' do diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index bf34c99b92a..69059dfa562 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -16,7 +16,7 @@ feature 'Merge request created from fork' do background do fork_project.team << [user, :master] - login_as user + gitlab_sign_in user end scenario 'user can access merge request' do @@ -56,7 +56,7 @@ feature 'Merge request created from fork' do visit_merge_request(merge_request) page.within('.merge-request-tabs') { click_link 'Pipelines' } - page.within('table.ci-table') do + page.within('.ci-table') do expect(page).to have_content pipeline.status expect(page).to have_content pipeline.id end diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb index 1723fb7d365..f2af3198319 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -8,7 +8,7 @@ describe 'Deleted source branch', feature: true, js: true do let(:merge_request) { create(:merge_request) } before do - login_as user + gitlab_sign_in user merge_request.project.team << [user, :master] merge_request.update!(source_branch: 'this-branch-does-not-exist') visit namespace_project_merge_request_path( diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index e23dc2cd940..989dfb71d10 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -20,7 +20,7 @@ feature 'Diff note avatars', feature: true, js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user end context 'discussion tab' do diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index 4d549f3bdbb..0f8ca6f90d1 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -19,7 +19,7 @@ feature 'Diff notes resolve', feature: true, js: true do context 'no discussions' do before do project.team << [user, :master] - login_as user + gitlab_sign_in user note.destroy visit_merge_request end @@ -33,7 +33,7 @@ feature 'Diff notes resolve', feature: true, js: true do context 'as authorized user' do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit_merge_request end @@ -402,7 +402,7 @@ feature 'Diff notes resolve', feature: true, js: true do before do project.team << [guest, :guest] - login_as guest + gitlab_sign_in guest end context 'someone elses merge request' do diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index 44013df3ea0..cb6cd6571a8 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -74,8 +74,7 @@ feature 'Diffs URL', js: true, feature: true do context 'as author' do it 'shows direct edit link' do - login_as(author_user) - + gitlab_sign_in(author_user) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax @@ -85,8 +84,7 @@ feature 'Diffs URL', js: true, feature: true do context 'as user who needs to fork' do it 'shows fork/cancel confirmation' do - login_as(user) - + gitlab_sign_in(user) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb index 9db235f35ba..88ae257236c 100644 --- a/spec/features/merge_requests/discussion_spec.rb +++ b/spec/features/merge_requests/discussion_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' feature 'Merge Request Discussions', feature: true do before do - login_as :admin + gitlab_sign_in :admin end describe "Diff discussions" do diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index c77a5c68bc6..804bf6967d6 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -8,7 +8,7 @@ feature 'Edit Merge Request', feature: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index 32a9082b9b9..9b677aeca0a 100644 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -26,7 +26,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do mr3.labels << feature project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_merge_requests_path(project.namespace, project) end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index 265a0cfc198..79bca0c9de2 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -15,7 +15,7 @@ feature 'Merge Request filtering by Milestone', feature: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end scenario 'filters by no Milestone', js: true do diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index d086be70d69..c12edf1fdf3 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -14,7 +14,7 @@ describe 'Filter merge requests', feature: true do before do project.team << [user, :master] group.add_developer(user) - login_as(user) + gitlab_sign_in(user) create(:merge_request, source_project: project, target_project: project) visit namespace_project_merge_requests_path(project.namespace, project) diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 00ef1ffdddc..1996c2fa09a 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -18,7 +18,7 @@ describe 'New/edit merge request', feature: true, js: true do context 'owned projects' do before do - login_as(user) + gitlab_sign_in(user) end context 'new merge request' do @@ -96,6 +96,13 @@ describe 'New/edit merge request', feature: true, js: true do .to end_with(merge_request_path(merge_request)) end end + + it 'description has autocomplete' do + find('#merge_request_description').native.send_keys('') + fill_in 'merge_request_description', with: '@' + + expect(page).to have_selector('.atwho-view') + end end context 'edit merge request' do @@ -157,13 +164,20 @@ describe 'New/edit merge request', feature: true, js: true do end end end + + it 'description has autocomplete' do + find('#merge_request_description').native.send_keys('') + fill_in 'merge_request_description', with: '@' + + expect(page).to have_selector('.atwho-view') + end end end context 'forked project' do before do fork_project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'new merge request' do diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb index 221ddb5873c..27ba380b005 100644 --- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb +++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb @@ -34,7 +34,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_merge_request_path(project.namespace, project, merge_request) diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb index c1d4d508e57..8af7d985036 100644 --- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb +++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb @@ -18,7 +18,9 @@ feature 'Merge immediately', :feature, :js do sha: project.repository.commit('master').id) end - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end context 'when there is active pipeline for merge request' do background do @@ -26,7 +28,7 @@ feature 'Merge immediately', :feature, :js do end before do - login_as user + gitlab_sign_in user visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) end diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb index 09f889d4dd6..bfadd7cb81a 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -28,7 +28,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do end before do - login_as user + gitlab_sign_in user visit_merge_request(merge_request) end @@ -121,7 +121,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do end before do - login_as user + gitlab_sign_in user visit_merge_request(merge_request) end diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index 3a11ea3c8b2..7664fbfbb4c 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -11,7 +11,7 @@ feature 'Mini Pipeline Graph', :js, :feature do before do build.run - login_as(user) + gitlab_sign_in(user) visit_merge_request end diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index b1dc81a606a..5cd9a7fbe26 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -5,7 +5,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu let(:project) { merge_request.target_project } before do - login_as merge_request.author + gitlab_sign_in merge_request.author project.team << [merge_request.author, :master] end diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb index 4c76004cb93..c2241317e04 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_requests/pipelines_spec.rb @@ -7,7 +7,7 @@ feature 'Pipelines for Merge Requests', feature: true, js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user end context 'with pipelines' do @@ -28,7 +28,7 @@ feature 'Pipelines for Merge Requests', feature: true, js: true do end wait_for_requests - expect(page).to have_selector('.pipeline-actions') + expect(page).to have_selector('.stage-cell') end end diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb index c154cf8ade9..4328d66c748 100644 --- a/spec/features/merge_requests/target_branch_spec.rb +++ b/spec/features/merge_requests/target_branch_spec.rb @@ -13,7 +13,7 @@ describe 'Target branch', feature: true, js: true do end before do - login_as user + gitlab_sign_in user project.team << [user, :master] end diff --git a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb index 0f98737b700..cba9a2cda99 100644 --- a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb +++ b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' feature 'Toggle Whitespace Changes', js: true, feature: true do before do - login_as :admin + gitlab_sign_in :admin merge_request = create(:merge_request) project = merge_request.source_project visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index 3acd3f6a8b3..c4c06e9a7a0 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -8,7 +8,7 @@ feature 'toggler_behavior', js: true, feature: true do let(:fragment_id) { "#note_#{note.id}" } before do - login_as :admin + gitlab_sign_in :admin project = merge_request.source_project page.current_window.resize_to(1000, 300) visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb index bcdfdf78a44..d0418c74699 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -7,7 +7,7 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'status', js: true do diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb index 14bc549c9f9..ac7e0eb2727 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -7,7 +7,7 @@ feature 'Merge requests > User posts diff notes', :js do before do project.add_developer(user) - login_as(user) + gitlab_sign_in(user) end let(:comment_button_class) { '.add-diff-note' } diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb index 22552529b9e..12f987e12ea 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_notes_spec.rb @@ -13,7 +13,7 @@ describe 'Merge requests > User posts notes', :js do end before do - login_as :admin + gitlab_sign_in :admin visit namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -22,8 +22,8 @@ describe 'Merge requests > User posts notes', :js do describe 'the note form' do it 'is valid' do is_expected.to have_css('.js-main-target-form', visible: true, count: 1) - expect(find('.js-main-target-form .js-comment-button').value). - to eq('Comment') + expect(find('.js-main-target-form .js-comment-button').value) + .to eq('Comment') page.within('.js-main-target-form') do expect(page).not_to have_link('Cancel') end @@ -123,8 +123,8 @@ describe 'Merge requests > User posts notes', :js do page.within("#note_#{note.id}") do is_expected.to have_css('.note_edited_ago') - expect(find('.note_edited_ago').text). - to match(/less than a minute ago/) + expect(find('.note_edited_ago').text) + .to match(/less than a minute ago/) end end end diff --git a/spec/features/merge_requests/user_sees_system_notes_spec.rb b/spec/features/merge_requests/user_sees_system_notes_spec.rb index 55d0f9d728c..0d88a8172b0 100644 --- a/spec/features/merge_requests/user_sees_system_notes_spec.rb +++ b/spec/features/merge_requests/user_sees_system_notes_spec.rb @@ -11,7 +11,7 @@ feature 'Merge requests > User sees system notes' do before do user = create(:user) private_project.add_developer(user) - login_as(user) + gitlab_sign_in(user) end it 'shows the system note' do 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 0e64a3e1a4b..71aa71e380e 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -1,14 +1,14 @@ require 'rails_helper' -feature 'Merge Requests > User uses slash commands', feature: true, js: true do - include SlashCommandsHelpers +feature 'Merge Requests > User uses quick actions', feature: true, js: true do + include QuickActionsHelpers let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request, source_project: project) } let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } - it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do + it_behaves_like 'issuable record that supports quick actions 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', target_branch: 'master' } } } end @@ -16,7 +16,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do describe 'merge-request-only commands' do before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -51,8 +51,8 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do let(:guest) { create(:user) } before do project.team << [guest, :guest] - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -97,8 +97,8 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do let(:guest) { create(:user) } before do project.team << [guest, :guest] - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end @@ -125,9 +125,9 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } before do - logout + gitlab_sign_out another_project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) end it 'changes target_branch in new merge_request' do @@ -181,8 +181,8 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do let(:guest) { create(:user) } before do project.team << [guest, :guest] - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb index aad522ee26e..04a72d3be34 100644 --- a/spec/features/merge_requests/versions_spec.rb +++ b/spec/features/merge_requests/versions_spec.rb @@ -8,7 +8,7 @@ feature 'Merge Request versions', js: true, feature: true do let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') } before do - login_as :admin + gitlab_sign_in :admin visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb index 118ecd9cba5..e82e69c5f4a 100644 --- a/spec/features/merge_requests/widget_deployments_spec.rb +++ b/spec/features/merge_requests/widget_deployments_spec.rb @@ -12,7 +12,7 @@ feature 'Widget Deployments Header', feature: true, js: true do given!(:manual) { } background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 4f3a5119915..3ac1f603de6 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -7,7 +7,7 @@ describe 'Merge request', :feature, :js do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'new merge request' do @@ -209,8 +209,8 @@ describe 'Merge request', :feature, :js do before do project.team << [user2, :master] - logout - login_as user2 + gitlab_sign_out + gitlab_sign_in user2 merge_request.update(target_project: fork_project) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_requests/wip_message_spec.rb index 3311731b33b..72d001bf408 100644 --- a/spec/features/merge_requests/wip_message_spec.rb +++ b/spec/features/merge_requests/wip_message_spec.rb @@ -6,7 +6,7 @@ feature 'Work In Progress help message', feature: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'with WIP commits' do diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index c07de01c594..58989581ffe 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -6,7 +6,7 @@ feature 'Milestone', feature: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end feature 'Create a milestone' do diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb deleted file mode 100644 index b3dfd6d0e81..00000000000 --- a/spec/features/milestones/milestones_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -require 'rails_helper' - -describe 'Milestone draggable', feature: true, js: true do - include DragTo - - let(:milestone) { create(:milestone, project: project, title: 8.14) } - let(:project) { create(:empty_project, :public) } - let(:user) { create(:user) } - - context 'issues' do - let(:issue) { page.find_by_id('issues-list-unassigned').find('li') } - let(:issue_target) { page.find_by_id('issues-list-ongoing') } - - it 'does not allow guest to drag issue' do - create_and_drag_issue - - expect(issue_target).not_to have_selector('.issuable-row') - end - - it 'does not allow authorized user to drag issue' do - login_as(user) - create_and_drag_issue - - expect(issue_target).not_to have_selector('.issuable-row') - end - - it 'allows author to drag issue' do - login_as(user) - create_and_drag_issue(author: user) - - expect(issue_target).to have_selector('.issuable-row') - end - - it 'allows admin to drag issue' do - login_as(:admin) - create_and_drag_issue - - expect(issue_target).to have_selector('.issuable-row') - end - end - - context 'merge requests' do - let(:merge_request) { page.find_by_id('merge_requests-list-unassigned').find('li') } - let(:merge_request_target) { page.find_by_id('merge_requests-list-ongoing') } - - it 'does not allow guest to drag merge request' do - create_and_drag_merge_request - - expect(merge_request_target).not_to have_selector('.issuable-row') - end - - it 'does not allow authorized user to drag merge request' do - login_as(user) - create_and_drag_merge_request - - expect(merge_request_target).not_to have_selector('.issuable-row') - end - - it 'allows author to drag merge request' do - login_as(user) - create_and_drag_merge_request(author: user) - - expect(merge_request_target).to have_selector('.issuable-row') - end - - it 'allows admin to drag merge request' do - login_as(:admin) - create_and_drag_merge_request - - expect(merge_request_target).to have_selector('.issuable-row') - end - end - - def create_and_drag_issue(params = {}) - create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone)) - - visit namespace_project_milestone_path(project.namespace, project, milestone) - scroll_into_view('.milestone-content') - drag_to(selector: '.issues-sortable-list', list_to_index: 1) - - wait_for_requests - end - - def create_and_drag_merge_request(params = {}) - create(:merge_request, params.merge(title: 'Foo', source_project: project, target_project: project, milestone: milestone)) - - visit namespace_project_milestone_path(project.namespace, project, milestone) - page.find("a[href='#tab-merge-requests']").click - - wait_for_requests - - scroll_into_view('.milestone-content') - drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1) - - wait_for_requests - end - - def scroll_into_view(selector) - page.evaluate_script("document.querySelector('#{selector}').scrollIntoView();") - end -end diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb index 227eb04ba72..cdf6cfba402 100644 --- a/spec/features/milestones/show_spec.rb +++ b/spec/features/milestones/show_spec.rb @@ -9,7 +9,7 @@ describe 'Milestone show', feature: true do before do project.add_user(user, :developer) - login_as(user) + gitlab_sign_in(user) end def visit_milestone diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index 449ce80bc71..b8966cf621c 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -8,7 +8,7 @@ feature 'Member autocomplete', :js do before do note # actually create the note - login_as(user) + gitlab_sign_in(user) end shared_examples "open suggestions when typing @" do diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 7df628fd7a0..bb4263d83f3 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -4,7 +4,7 @@ describe 'Profile account page', feature: true do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) end describe 'when signup is enabled' do diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb index 05a7587f8d4..33fd29b429b 100644 --- a/spec/features/profiles/account_spec.rb +++ b/spec/features/profiles/account_spec.rb @@ -4,7 +4,7 @@ feature 'Profile > Account', feature: true do given(:user) { create(:user, username: 'foo') } before do - login_as(user) + gitlab_sign_in(user) end describe 'Change username' do @@ -31,8 +31,13 @@ feature 'Profile > Account', feature: true do given(:new_project_path) { "/#{new_username}/#{project.path}" } given(:old_project_path) { "/#{user.username}/#{project.path}" } - before(:context) { TestEnv.clean_test_path } - after(:example) { TestEnv.clean_test_path } + before(:context) do + TestEnv.clean_test_path + end + + after(:example) do + TestEnv.clean_test_path + end scenario 'the project is accessible via the new path' do update_username(new_username) diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb index 6f6f7029c0b..1a162d6be0e 100644 --- a/spec/features/profiles/chat_names_spec.rb +++ b/spec/features/profiles/chat_names_spec.rb @@ -5,7 +5,7 @@ feature 'Profile > Chat', feature: true do given(:service) { create(:service) } before do - login_as(user) + gitlab_sign_in(user) end describe 'uses authorization link' do diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index 2f436f153aa..13f9afd4ce0 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -4,7 +4,7 @@ feature 'Profile > SSH Keys', feature: true do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) end describe 'User adds a key' do diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb index 1a5a9059dbd..a6f9beafe17 100644 --- a/spec/features/profiles/oauth_applications_spec.rb +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -4,7 +4,7 @@ describe 'Profile > Applications', feature: true do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) end describe 'User manages applications', js: true do diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb index 4cbdd89d46f..86c9df5ff86 100644 --- a/spec/features/profiles/password_spec.rb +++ b/spec/features/profiles/password_spec.rb @@ -4,7 +4,7 @@ describe 'Profile > Password', feature: true do let(:user) { create(:user, password_automatically_set: true) } before do - login_as(user) + gitlab_sign_in(user) visit edit_profile_password_path end @@ -25,7 +25,7 @@ describe 'Profile > Password', feature: true do end end - it 'does not contains the current password field after an error' do + it 'does not contain the current password field after an error' do fill_passwords('mypassword', 'mypassword2') expect(page).to have_no_field('user[current_password]') diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 7e2e685df26..d7acaaf1eb8 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -23,7 +23,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do end before do - login_as(user) + gitlab_sign_in(user) end describe "token creation" do diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index d368bc4d753..8e7ef6bc110 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -4,7 +4,7 @@ describe 'Profile > Preferences', feature: true do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) visit profile_preferences_path end diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb index e05fbb3715c..c0092836e3b 100644 --- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb +++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb @@ -4,7 +4,7 @@ feature 'Profile > Notifications > User changes notified_of_own_activity setting let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) end scenario 'User opts into receiving notifications about their own activity' do diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb new file mode 100644 index 00000000000..e98cec79d87 --- /dev/null +++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'User visits the notifications tab', js: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + visit(profile_notifications_path) + end + + it 'changes the project notifications setting' do + expect(page).to have_content('Notifications') + + first('#notifications-button').trigger('click') + click_link('On mention') + + expect(page).to have_content('On mention') + end +end diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb index 3c1de5c09b2..84c81d43448 100644 --- a/spec/features/projects/activity/rss_spec.rb +++ b/spec/features/projects/activity/rss_spec.rb @@ -12,7 +12,7 @@ feature 'Project Activity RSS' do before do user = create(:user) project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit path end diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb index 01a95bf49ac..9624e1a71b0 100644 --- a/spec/features/projects/badges/coverage_spec.rb +++ b/spec/features/projects/badges/coverage_spec.rb @@ -7,7 +7,7 @@ feature 'test coverage badge' do context 'when user has access to view badge' do background do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) end scenario 'user requests coverage badge image for pipeline' do @@ -45,7 +45,7 @@ feature 'test coverage badge' do end context 'when user does not have access to view badge' do - background { login_as(user) } + background { gitlab_sign_in(user) } scenario 'user requests test coverage badge image' do show_test_coverage_badge diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index ae9db0c0d6e..348748152bb 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -5,7 +5,7 @@ feature 'list of badges' do user = create(:user) project = create(:project) project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_pipelines_settings_path(project.namespace, project) end diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb index 53c5a52ce3a..d94204230f6 100644 --- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb +++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb @@ -55,7 +55,7 @@ feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true, end end - describe 'Click "Annotate" button' do + describe 'Click "Blame" button' do it 'works with no initial line number fragment hash' do visit_blob diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 45fdb36e506..71ffa352f80 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -17,6 +17,7 @@ feature 'File blob', :js, feature: true do it 'displays the blob' do aggregate_failures do # shows highlighted Ruby code + expect(page).to have_css(".js-syntax-highlight") expect(page).to have_content("require 'fileutils'") # does not show a viewer switcher @@ -71,6 +72,7 @@ feature 'File blob', :js, feature: true do expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) # shows highlighted Markdown code + expect(page).to have_css(".js-syntax-highlight") expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") # shows an enabled copy button @@ -114,6 +116,7 @@ feature 'File blob', :js, feature: true do expect(page).to have_selector('#LC1.hll') # shows highlighted Markdown code + expect(page).to have_css(".js-syntax-highlight") expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") # shows an enabled copy button diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index d04c3248ead..d0bc032ee93 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -14,7 +14,7 @@ feature 'Editing file blob', feature: true, js: true do before do project.team << [user, role] - login_as(user) + gitlab_sign_in(user) end def edit_and_commit @@ -61,7 +61,7 @@ feature 'Editing file blob', feature: true, js: true do it 'redirects to sign in and returns' do expect(page).to have_current_path(new_user_session_path) - login_as(user) + gitlab_sign_in(user) expect(page).to have_current_path(namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path))) end @@ -77,7 +77,7 @@ feature 'Editing file blob', feature: true, js: true do it 'redirects to sign in and returns' do expect(page).to have_current_path(new_user_session_path) - login_as(user) + gitlab_sign_in(user) expect(page).to have_current_path(namespace_project_blob_path(project.namespace, project, tree_join(branch, file_path))) end @@ -92,7 +92,7 @@ feature 'Editing file blob', feature: true, js: true do project.team << [user, :developer] project.repository.add_branch(user, protected_branch, 'master') create(:protected_branch, project: project, name: protected_branch) - login_as(user) + gitlab_sign_in(user) end context 'on some branch' do @@ -122,7 +122,7 @@ feature 'Editing file blob', feature: true, js: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path)) end diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index 92028c19361..d8c4d475a2c 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -22,7 +22,7 @@ feature 'Download buttons in branches page', feature: true do end background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] end diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb index c5e0a0f0517..406fa52e723 100644 --- a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb +++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb @@ -8,7 +8,7 @@ describe 'New Branch Ref Dropdown', :js, :feature do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit new_namespace_project_branch_path(project.namespace, project) end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 7668ce5f8be..8694366de35 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Branches', feature: true do + let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:repository) { project.repository } @@ -12,8 +13,8 @@ describe 'Branches', feature: true do context 'logged in as developer' do before do - login_as :user - project.team << [@user, :developer] + sign_in(user) + project.team << [user, :developer] end describe 'Initial branches page' do @@ -27,7 +28,7 @@ describe 'Branches', feature: true do it 'avoids a N+1 query in branches index' do control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count - %w(one two three four five).each { |ref| repository.add_branch(@user, ref, 'master') } + %w(one two three four five).each { |ref| repository.add_branch(user, ref, 'master') } expect { visit namespace_project_branches_path(project.namespace, project) }.not_to exceed_query_limit(control_count) end @@ -64,14 +65,14 @@ describe 'Branches', feature: true do describe 'Delete protected branch' do before do - project.add_user(@user, :master) + project.add_user(user, :master) visit namespace_project_protected_branches_path(project.namespace, project) set_protected_branch_name('fix') click_on "Protect" within(".protected-branches-list") { expect(page).to have_content('fix') } expect(ProtectedBranch.count).to eq(1) - project.add_user(@user, :developer) + project.add_user(user, :developer) end it 'does not allow devleoper to removes protected branch', js: true do @@ -87,8 +88,8 @@ describe 'Branches', feature: true do context 'logged in as master' do before do - login_as :user - project.team << [@user, :master] + sign_in(user) + project.team << [user, :master] end describe 'Delete protected branch' do diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 268d420c594..e5b1f95f2b9 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -6,7 +6,7 @@ feature 'project commit pipelines', js: true do background do user = create(:user) project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'when no builds triggered yet' do diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index bc7ca0ddd38..0d3fa72fbf5 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -1,14 +1,15 @@ require 'spec_helper' describe 'Cherry-pick Commits' do + let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, namespace: group) } let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') } before do - login_as :user - project.team << [@user, :master] + sign_in(user) + project.team << [user, :master] visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) end diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb index f2de195eb7f..570a7ae7b16 100644 --- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb +++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb @@ -5,7 +5,7 @@ feature 'Mini Pipeline Graph in Commit View', :js, :feature do let(:project) { create(:project, :public) } before do - login_as(user) + gitlab_sign_in(user) end context 'when commit has pipelines' do diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commit/rss_spec.rb index 03b6d560c96..f7548a56984 100644 --- a/spec/features/projects/commit/rss_spec.rb +++ b/spec/features/projects/commit/rss_spec.rb @@ -8,7 +8,7 @@ feature 'Project Commits RSS' do before do user = create(:user) project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit path end diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index ee6985ad993..4743d69fb75 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -6,7 +6,7 @@ describe "Compare", js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_compare_index_path(project.namespace, project, from: "master", to: "master") end diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb index 06abfbbc86b..a31960639fe 100644 --- a/spec/features/projects/deploy_keys_spec.rb +++ b/spec/features/projects/deploy_keys_spec.rb @@ -6,7 +6,7 @@ describe 'Project deploy keys', :js, :feature do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end describe 'removing key' do diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb index 0c51fe72ca4..a943f1e6a08 100644 --- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -7,7 +7,7 @@ feature 'Developer views empty project instructions', feature: true do background do project.team << [developer, :developer] - login_as(developer) + gitlab_sign_in(developer) end context 'without an SSH key' do diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb new file mode 100644 index 00000000000..48b7f1e0f34 --- /dev/null +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +feature 'Diff file viewer', :js, feature: true do + let(:project) { create(:project, :public, :repository) } + + def visit_commit(sha, anchor: nil) + visit namespace_project_commit_path(project.namespace, project, sha, anchor: anchor) + + wait_for_requests + end + + context 'Ruby file' do + before do + visit_commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + end + + it 'shows highlighted Ruby code' do + within('.diff-file[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do + expect(page).to have_css(".js-syntax-highlight") + expect(page).to have_content("def popen(cmd, path=nil)") + end + end + end + + context 'Ruby file (stored in LFS)' do + before do + project.add_master(project.creator) + + @commit_id = Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add Ruby file in LFS", + file_path: 'files/lfs/ruby.rb', + file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data + ).execute[:result] + end + + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_commit(@commit_id) + end + + it 'shows an error message' do + expect(page).to have_content('This source diff could not be displayed because it is stored in LFS. You can view the blob instead.') + end + end + + context 'when LFS is disabled on the project' do + before do + visit_commit(@commit_id) + end + + it 'displays the diff' do + expect(page).to have_content('size 1575078') + end + end + end + + context 'Image file' do + before do + visit_commit('2f63565e7aac07bcdadb654e253078b727143ec4') + end + + it 'shows a rendered image' do + within('.diff-file[id="e986451b8f7397b617dbb6fffcb5539328c56921"]') do + expect(page).to have_css('img[alt="files/images/6049019_460s.jpg"]') + end + end + end + + context 'ISO file (stored in LFS)' do + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_commit('048721d90c449b244b7b4c53a9186b04330174ec') + end + + it 'shows that file was added' do + expect(page).to have_content('File added') + end + end + + context 'when LFS is disabled on the project' do + before do + visit_commit('048721d90c449b244b7b4c53a9186b04330174ec') + end + + it 'displays the diff' do + expect(page).to have_content('size 1575078') + end + end + end + + context 'ZIP file' do + before do + visit_commit('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') + end + + it 'shows that file was added' do + expect(page).to have_content('File added') + end + end + + context 'binary file that appears to be text in the first 1024 bytes' do + before do + visit_commit('7b1cf4336b528e0f3d1d140ee50cafdbc703597c') + end + + it 'shows the diff is collapsed' do + expect(page).to have_content('This diff is collapsed. Click to expand it.') + end + + context 'expanding the diff' do + before do + # We can't use `click_link` because the "link" doesn't have an `href`. + find('a.click-to-expand').click + + wait_for_requests + end + + it 'shows there is no preview' do + expect(page).to have_content('No preview for this file type') + end + end + end +end diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb index a263781c43c..ca202b95a44 100644 --- a/spec/features/projects/edit_spec.rb +++ b/spec/features/projects/edit_spec.rb @@ -6,7 +6,7 @@ feature 'Project edit', feature: true, js: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit edit_namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb index ee925e811e1..b48dcf6c774 100644 --- a/spec/features/projects/environments/environment_metrics_spec.rb +++ b/spec/features/projects/environments/environment_metrics_spec.rb @@ -15,7 +15,7 @@ feature 'Environment > Metrics', :feature do create(:deployment, environment: environment, deployable: build) stub_all_prometheus_requests(environment.slug) - login_as(user) + gitlab_sign_in(user) visit_environment(environment) end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 18b608c863e..7d565555f1f 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -6,7 +6,7 @@ feature 'Environment', :feature do given(:role) { :developer } background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 613b1edba36..cf4d996a32d 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -7,7 +7,7 @@ feature 'Environments page', :feature, :js do background do project.team << [user, role] - login_as(user) + gitlab_sign_in(user) end given!(:environment) { } @@ -151,7 +151,7 @@ feature 'Environments page', :feature, :js do find('.js-dropdown-play-icon-container').click expect(page).to have_content(action.name.humanize) - expect { find('.js-manual-action-link').click } + expect { find('.js-manual-action-link').trigger('click') } .not_to change { Ci::Pipeline.count } end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index c49648f54bd..db2790a4bce 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -9,7 +9,7 @@ describe 'Edit Project Settings', feature: true do describe 'project features visibility selectors', js: true do before do project.team << [member, :master] - login_as(member) + gitlab_sign_in(member) end tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests" } @@ -68,9 +68,12 @@ describe 'Edit Project Settings', feature: true do end describe 'project features visibility pages' do + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:job) { create(:ci_build, pipeline: pipeline) } + let(:tools) do { - builds: namespace_project_pipelines_path(project.namespace, project), + builds: namespace_project_job_path(project.namespace, project, job), issues: namespace_project_issues_path(project.namespace, project), wiki: namespace_project_wiki_path(project.namespace, project, :home), snippets: namespace_project_snippets_path(project.namespace, project), @@ -80,7 +83,7 @@ describe 'Edit Project Settings', feature: true do context 'normal user' do before do - login_as(member) + gitlab_sign_in(member) end it 'renders 200 if tool is enabled' do @@ -127,7 +130,7 @@ describe 'Edit Project Settings', feature: true do context 'admin user' do before do non_member.update_attribute(:admin, true) - login_as(non_member) + gitlab_sign_in(non_member) end it 'renders 404 if feature is disabled' do @@ -153,7 +156,7 @@ describe 'Edit Project Settings', feature: true do describe 'repository visibility', js: true do before do project.team << [member, :master] - login_as(member) + gitlab_sign_in(member) visit edit_namespace_project_path(project.namespace, project) end @@ -239,7 +242,7 @@ describe 'Edit Project Settings', feature: true do before do project.team << [member, :guest] - login_as(member) + gitlab_sign_in(member) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb index 30a1eedbb48..34aef958ec6 100644 --- a/spec/features/projects/files/browse_files_spec.rb +++ b/spec/features/projects/files/browse_files_spec.rb @@ -6,13 +6,13 @@ feature 'user browses project', feature: true, js: true do before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) visit namespace_project_tree_path(project.namespace, project, project.default_branch) end scenario "can see blame of '.gitignore'" do click_link ".gitignore" - click_link 'Annotate' + click_link 'Blame' expect(page).to have_content "*.rb" expect(page).to have_content "Dmitriy Zaporozhets" diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb index 69744ac3948..2a1cc01fe68 100644 --- a/spec/features/projects/files/creating_a_file_spec.rb +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -6,7 +6,7 @@ feature 'User wants to create a file', feature: true do background do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_new_blob_path(project.namespace, project, project.default_branch) end diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb index 93909e91d05..4f1b8588462 100644 --- a/spec/features/projects/files/dockerfile_dropdown_spec.rb +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -7,7 +7,7 @@ feature 'User wants to add a Dockerfile file', feature: true do project = create(:project) project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile') end diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb index d7c29a7e074..60182bfebe9 100644 --- a/spec/features/projects/files/download_buttons_spec.rb +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -22,7 +22,7 @@ feature 'Download buttons in files tree', feature: true do end background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] end diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb index 012befa7990..6e361ac4312 100644 --- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb +++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb @@ -5,7 +5,7 @@ feature 'User uses soft wrap whilst editing file', feature: true, js: true do user = create(:user) project = create(:project) project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'test_file-name') editor = find('.file-editor.code') editor.click diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index 7a3afafec29..e97ff5fded7 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -17,7 +17,7 @@ feature 'User wants to edit a file', feature: true do background do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, '.gitignore')) end diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb index 5c8105de4cb..83a837fba44 100644 --- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb +++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb @@ -6,7 +6,7 @@ feature 'User views files page', feature: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref) end diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb index ee42bcaec4b..6a914820ac9 100644 --- a/spec/features/projects/files/find_file_keyboard_spec.rb +++ b/spec/features/projects/files/find_file_keyboard_spec.rb @@ -6,7 +6,7 @@ feature 'Find file keyboard shortcuts', feature: true, js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_find_file_path(project.namespace, project, project.repository.root_ref) diff --git a/spec/features/projects/files/find_files_spec.rb b/spec/features/projects/files/find_files_spec.rb index 716b7591b95..166ec5c921b 100644 --- a/spec/features/projects/files/find_files_spec.rb +++ b/spec/features/projects/files/find_files_spec.rb @@ -5,7 +5,7 @@ feature 'Find files button in the tree header', feature: true do given(:project) { create(:project) } background do - login_as(user) + gitlab_sign_in(user) project.team << [user, :developer] end diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb index e9f49453121..7f02ec6b73d 100644 --- a/spec/features/projects/files/gitignore_dropdown_spec.rb +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -5,7 +5,7 @@ feature 'User wants to add a .gitignore file', feature: true do user = create(:user) project = create(:project) project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore') end diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb index 031b89d0499..f4b17c2518c 100644 --- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb +++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb @@ -5,7 +5,7 @@ feature 'User wants to add a .gitlab-ci.yml file', feature: true do user = create(:user) project = create(:project) project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitlab-ci.yml') end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 8d410cc3f2e..7daf016dd22 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -7,7 +7,7 @@ feature 'project owner creates a license file', feature: true, js: true do project.repository.delete_file(project_master, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') project.team << [project_master, :master] - login_as(project_master) + gitlab_sign_in(project_master) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 8e197bccabf..eab19d52030 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -5,7 +5,7 @@ feature 'project owner sees a link to create a license file in empty project', f let(:project) { create(:empty_project) } background do project.team << [project_master, :master] - login_as(project_master) + gitlab_sign_in(project_master) end scenario 'project master creates a license file from a template' do diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb index 9fcf12e6cb9..028a0919640 100644 --- a/spec/features/projects/files/template_type_dropdown_spec.rb +++ b/spec/features/projects/files/template_type_dropdown_spec.rb @@ -6,7 +6,7 @@ feature 'Template type dropdown selector', js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user end context 'editing a non-matching file' do diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb index de10eec0557..4ccd123f46e 100644 --- a/spec/features/projects/files/undo_template_spec.rb +++ b/spec/features/projects/files/undo_template_spec.rb @@ -6,7 +6,7 @@ feature 'Template Undo Button', js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user end context 'editing a matching file and applying a template' do diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb index 67bc9142356..aa4ed217a34 100644 --- a/spec/features/projects/gfm_autocomplete_load_spec.rb +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -4,7 +4,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do let(:project) { create(:project) } before do - login_as :admin + gitlab_sign_in :admin visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index 1b680a56492..778f5d61ae3 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -9,7 +9,7 @@ feature 'Project group links', :feature, :js do background do project.add_master(master) - login_as(master) + gitlab_sign_in(master) end context 'setting an expiration date for a group link' do diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb index b91c3eff478..e1f7f06c113 100644 --- a/spec/features/projects/guest_navigation_menu_spec.rb +++ b/spec/features/projects/guest_navigation_menu_spec.rb @@ -7,7 +7,7 @@ describe 'Guest navigation menu' do before do project.team << [guest, :guest] - login_as(guest) + gitlab_sign_in(guest) end it 'shows allowed tabs only' do diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 40caf89dd54..b5c64777934 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -33,7 +33,7 @@ feature 'Import/Export - project export integration test', feature: true, js: tr context 'admin user' do before do - login_as(user) + gitlab_sign_in(user) end scenario 'exports a project successfully' do diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 583f479ec18..a111aa87c52 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -19,7 +19,7 @@ feature 'Import/Export - project import integration test', feature: true, js: tr let!(:namespace) { create(:namespace, name: "asd", owner: user) } before do - login_as(user) + gitlab_sign_in(user) end scenario 'user imports an exported project successfully' do @@ -77,7 +77,7 @@ feature 'Import/Export - project import integration test', feature: true, js: tr context 'when limited to the default user namespace' do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) end scenario 'passes correct namespace ID in the URL' do diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb index cb399ea55df..b0a68f0d61f 100644 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -16,7 +16,7 @@ feature 'Import/Export - Namespace export file cleanup', feature: true, js: true context 'admin user' do before do - login_as(:admin) + gitlab_sign_in(:admin) end context 'moving the namespace' do diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 4efd5a26a82..e03e7b88174 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 3076c863dcb..26a09985312 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -6,7 +6,7 @@ feature 'issuable templates', feature: true, js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user end context 'user creates an issue using templates' do @@ -124,11 +124,11 @@ feature 'issuable templates', feature: true, js: true do let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) } background do - logout + gitlab_sign_out project.team << [fork_user, :developer] fork_project.team << [fork_user, :master] create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) - login_as fork_user + gitlab_sign_in fork_user project.repository.create_file( fork_user, '.gitlab/merge_request_templates/feature-proposal.md', diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb index 3137af074ca..b2db07a75ef 100644 --- a/spec/features/projects/issues/list_spec.rb +++ b/spec/features/projects/issues/list_spec.rb @@ -7,7 +7,7 @@ feature 'Issues List' do background do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) end scenario 'user does not see create new list button' do diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb index f6852192aef..38733d39932 100644 --- a/spec/features/projects/issues/rss_spec.rb +++ b/spec/features/projects/issues/rss_spec.rb @@ -12,7 +12,7 @@ feature 'Project Issues RSS' do before do user = create(:user) project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit path end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 727ae7081b0..070cdbf1cef 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -8,8 +8,8 @@ feature 'Jobs', :feature do let(:namespace) { project.namespace } let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } - let(:build2) { create(:ci_build) } + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } + let(:job2) { create(:ci_build) } let(:artifacts_file) do fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') @@ -17,11 +17,11 @@ feature 'Jobs', :feature do before do project.team << [user, user_access_level] - login_as(user) + gitlab_sign_in(user) end describe "GET /:project/jobs" do - let!(:build) { create(:ci_build, pipeline: pipeline) } + let!(:job) { create(:ci_build, pipeline: pipeline) } context "Pending scope" do before do @@ -31,30 +31,30 @@ feature 'Jobs', :feature do it "shows Pending tab jobs" do expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') - expect(page).to have_content build.short_sha - expect(page).to have_content build.ref - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name end end context "Running scope" do before do - build.run! + job.run! visit namespace_project_jobs_path(project.namespace, project, scope: :running) end it "shows Running tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_link 'Cancel running' - expect(page).to have_content build.short_sha - expect(page).to have_content build.ref - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name end end context "Finished scope" do before do - build.run! + job.run! visit namespace_project_jobs_path(project.namespace, project, scope: :finished) end @@ -73,9 +73,9 @@ feature 'Jobs', :feature do it "shows All tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content build.short_sha - expect(page).to have_content build.ref - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name expect(page).not_to have_link 'Cancel running' end end @@ -97,7 +97,7 @@ feature 'Jobs', :feature do describe "POST /:project/jobs/:id/cancel_all" do before do - build.run! + job.run! visit namespace_project_jobs_path(project.namespace, project) click_link "Cancel running" end @@ -105,19 +105,19 @@ feature 'Jobs', :feature do it 'shows all necessary content' do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content 'canceled' - expect(page).to have_content build.short_sha - expect(page).to have_content build.ref - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name expect(page).not_to have_link 'Cancel running' end end describe "GET /:project/jobs/:id" do context "Job from project" do - let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:job) { create(:ci_build, :success, pipeline: pipeline) } before do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end it 'shows status name', :js do @@ -131,33 +131,33 @@ feature 'Jobs', :feature do expect(page).to have_content pipeline.git_author_name end - it 'shows active build' do + it 'shows active job' do expect(page).to have_selector('.build-job.active') end end context 'when job is not running', :js do - let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:job) { create(:ci_build, :success, pipeline: pipeline) } before do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end it 'shows retry button' do expect(page).to have_link('Retry') end - context 'if build passed' do + context 'if job passed' do it 'does not show New issue button' do expect(page).not_to have_link('New issue') end end - context 'if build failed' do - let(:build) { create(:ci_build, :failed, pipeline: pipeline) } + context 'if job failed' do + let(:job) { create(:ci_build, :failed, pipeline: pipeline) } before do - visit namespace_project_job_path(namespace, project, build) + visit namespace_project_job_path(namespace, project, job) end it 'shows New issue button' do @@ -165,9 +165,9 @@ feature 'Jobs', :feature do end it 'links to issues/new with the title and description filled in' do - button_title = "Build Failed ##{build.id}" - build_path = namespace_project_job_path(namespace, project, build) - options = { issue: { title: button_title, description: build_path } } + button_title = "Build Failed ##{job.id}" + job_path = namespace_project_job_path(namespace, project, job) + options = { issue: { title: button_title, description: job_path } } href = new_namespace_project_issue_path(namespace, project, options) @@ -180,7 +180,7 @@ feature 'Jobs', :feature do context "Job from other project" do before do - visit namespace_project_job_path(project.namespace, project, build2) + visit namespace_project_job_path(project.namespace, project, job2) end it { expect(page.status_code).to eq(404) } @@ -188,8 +188,8 @@ feature 'Jobs', :feature do context "Download artifacts" do before do - build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_job_path(project.namespace, project, build) + job.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_job_path(project.namespace, project, job) end it 'has button to download artifacts' do @@ -199,10 +199,10 @@ feature 'Jobs', :feature do context 'Artifacts expire date' do before do - build.update_attributes(artifacts_file: artifacts_file, - artifacts_expire_at: expire_at) + job.update_attributes(artifacts_file: artifacts_file, + artifacts_expire_at: expire_at) - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end context 'no expire date defined' do @@ -248,7 +248,7 @@ feature 'Jobs', :feature do context "when visiting old URL" do let(:job_url) do - namespace_project_job_path(project.namespace, project, build) + namespace_project_job_path(project.namespace, project, job) end before do @@ -262,9 +262,9 @@ feature 'Jobs', :feature do feature 'Raw trace' do before do - build.run! + job.run! - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end it do @@ -274,16 +274,16 @@ feature 'Jobs', :feature do feature 'HTML trace', :js do before do - build.run! + job.run! - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end context 'when job has an initial trace' do it 'loads job trace' do expect(page).to have_content 'BUILD TRACE' - build.trace.write do |stream| + job.trace.write do |stream| stream.append(' and more trace', 11) end @@ -295,12 +295,12 @@ feature 'Jobs', :feature do feature 'Variables' do let(:trigger_request) { create(:ci_trigger_request_with_variables) } - let(:build) do + let(:job) do create :ci_build, pipeline: pipeline, trigger_request: trigger_request end before do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end it 'shows variable key and value after click', js: true do @@ -322,20 +322,20 @@ feature 'Jobs', :feature do context 'job is successfull and has deployment' do let(:deployment) { create(:deployment) } - let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } + let(:job) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } it 'shows a link for the job' do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) expect(page).to have_link environment.name end end context 'job is complete and not successful' do - let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } + let(:job) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } it 'shows a link for the job' do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) expect(page).to have_link environment.name end @@ -343,10 +343,10 @@ feature 'Jobs', :feature do context 'job creates a new deployment' do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } - let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } it 'shows a link to latest deployment' do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) expect(page).to have_link('latest deployment') end @@ -357,8 +357,8 @@ feature 'Jobs', :feature do describe "POST /:project/jobs/:id/cancel", :js do context "Job from project" do before do - build.run! - visit namespace_project_job_path(project.namespace, project, build) + job.run! + visit namespace_project_job_path(project.namespace, project, job) find('.js-cancel-job').click() end @@ -372,8 +372,8 @@ feature 'Jobs', :feature do describe "POST /:project/jobs/:id/retry" do context "Job from project", :js do before do - build.run! - visit namespace_project_job_path(project.namespace, project, build) + job.run! + visit namespace_project_job_path(project.namespace, project, job) find('.js-cancel-job').click() find('.js-retry-button').trigger('click') end @@ -388,13 +388,13 @@ feature 'Jobs', :feature do context "Job that current user is not allowed to retry" do before do - build.run! - build.cancel! + job.run! + job.cancel! project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - logout_direct - login_with(create(:user)) - visit namespace_project_job_path(project.namespace, project, build) + gitlab_sign_out_direct + gitlab_sign_in(create(:user)) + visit namespace_project_job_path(project.namespace, project, job) end it 'does not show the Retry button' do @@ -407,15 +407,15 @@ feature 'Jobs', :feature do describe "GET /:project/jobs/:id/download" do before do - build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_job_path(project.namespace, project, build) + job.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_job_path(project.namespace, project, job) click_link 'Download' end context "Build from other project" do before do - build2.update_attributes(artifacts_file: artifacts_file) - visit download_namespace_project_job_artifacts_path(project.namespace, project, build2) + job2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_job_artifacts_path(project.namespace, project, job2) end it { expect(page.status_code).to eq(404) } @@ -427,23 +427,23 @@ feature 'Jobs', :feature do context 'job from project' do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } - build.run! - visit namespace_project_job_path(project.namespace, project, build) + job.run! + visit namespace_project_job_path(project.namespace, project, job) find('.js-raw-link-controller').click() end it 'sends the right headers' do expect(page.status_code).to eq(200) expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(build.trace.send(:current_path)) + expect(page.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path)) end end context 'job from other project' do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } - build2.run! - visit raw_namespace_project_job_path(project.namespace, project, build2) + job2.run! + visit raw_namespace_project_job_path(project.namespace, project, job2) end it 'sends the right headers' do @@ -458,16 +458,16 @@ feature 'Jobs', :feature do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } - build.run! + job.run! end - context 'when build has trace in file', :js do + context 'when job has trace in file', :js do before do allow_any_instance_of(Gitlab::Ci::Trace) .to receive(:paths) .and_return([existing_file]) - visit namespace_project_job_path(namespace, project, build) + visit namespace_project_job_path(namespace, project, job) find('.js-raw-link-controller').click end @@ -485,7 +485,7 @@ feature 'Jobs', :feature do .to receive(:paths) .and_return([]) - visit namespace_project_job_path(namespace, project, build) + visit namespace_project_job_path(namespace, project, job) end it 'sends the right headers' do @@ -496,7 +496,7 @@ feature 'Jobs', :feature do context "when visiting old URL" do let(:raw_job_url) do - raw_namespace_project_job_path(project.namespace, project, build) + raw_namespace_project_job_path(project.namespace, project, job) end before do @@ -512,7 +512,7 @@ feature 'Jobs', :feature do describe "GET /:project/jobs/:id/trace.json" do context "Job from project" do before do - visit trace_namespace_project_job_path(project.namespace, project, build, format: :json) + visit trace_namespace_project_job_path(project.namespace, project, job, format: :json) end it { expect(page.status_code).to eq(200) } @@ -520,7 +520,7 @@ feature 'Jobs', :feature do context "Job from other project" do before do - visit trace_namespace_project_job_path(project.namespace, project, build2, format: :json) + visit trace_namespace_project_job_path(project.namespace, project, job2, format: :json) end it { expect(page.status_code).to eq(404) } @@ -530,7 +530,7 @@ feature 'Jobs', :feature do describe "GET /:project/jobs/:id/status" do context "Job from project" do before do - visit status_namespace_project_job_path(project.namespace, project, build) + visit status_namespace_project_job_path(project.namespace, project, job) end it { expect(page.status_code).to eq(200) } @@ -538,7 +538,7 @@ feature 'Jobs', :feature do context "Job from other project" do before do - visit status_namespace_project_job_path(project.namespace, project, build2) + visit status_namespace_project_job_path(project.namespace, project, job2) end it { expect(page.status_code).to eq(404) } diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index e2911a37e40..2c47758f30e 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -28,7 +28,7 @@ feature 'Issue prioritization', feature: true do issue_2.labels << label_4 issue_1.labels << label_5 - login_as user + gitlab_sign_in user visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority') # Ensure we are indicating that issues are sorted by priority @@ -67,7 +67,7 @@ feature 'Issue prioritization', feature: true do issue_4.labels << label_4 # 7 issue_6.labels << label_5 # 8 - No priority - login_as user + gitlab_sign_in user visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority') expect(page).to have_selector('.dropdown-toggle', text: 'Label priority') diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb index 3130d87fba5..584dc294f05 100644 --- a/spec/features/projects/labels/subscription_spec.rb +++ b/spec/features/projects/labels/subscription_spec.rb @@ -10,7 +10,7 @@ feature 'Labels subscription', feature: true do context 'when signed in' do before do project.team << [user, :developer] - login_as user + gitlab_sign_in user end scenario 'users can subscribe/unsubscribe to labels', js: true do diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 34fafe072a3..589bfb9fbc9 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -14,7 +14,7 @@ feature 'Prioritize labels', feature: true do before do project.team << [user, :developer] - login_as user + gitlab_sign_in user end scenario 'user can prioritize a group label', js: true do @@ -120,7 +120,7 @@ feature 'Prioritize labels', feature: true do it 'does not prioritize labels' do guest = create(:user) - login_as guest + gitlab_sign_in guest visit namespace_project_labels_path(project.namespace, project) diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb index 02198ff3e41..514453db472 100644 --- a/spec/features/projects/main/download_buttons_spec.rb +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -22,7 +22,7 @@ feature 'Download buttons in project main page', feature: true do end background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] end diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb index 53966229a2a..fee8cfe2c33 100644 --- a/spec/features/projects/main/rss_spec.rb +++ b/spec/features/projects/main/rss_spec.rb @@ -8,7 +8,7 @@ feature 'Project RSS' do before do user = create(:user) project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit path end diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb index 3d253f01484..00d2a27597b 100644 --- a/spec/features/projects/members/group_links_spec.rb +++ b/spec/features/projects/members/group_links_spec.rb @@ -9,7 +9,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t project.team << [user, :master] @group_link = create(:project_group_link, project: project, group: group) - login_as(user) + gitlab_sign_in(user) visit namespace_project_settings_members_path(project.namespace, project) end diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb index b483ba4c54c..7e71dbc24c0 100644 --- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb +++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb @@ -7,7 +7,7 @@ feature 'Projects > Members > Group member cannot leave group project', feature: background do group.add_developer(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb index ff9b6007806..60a5cd9ec63 100644 --- a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb +++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb @@ -41,7 +41,7 @@ feature 'Projects > Members > Group member cannot request access to his group pr end def login_and_visit_project_page(user) - login_as(user) + gitlab_sign_in(user) visit namespace_project_path(project.namespace, project) end end diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb index 3385e5972ff..76fe6a00dab 100644 --- a/spec/features/projects/members/group_members_spec.rb +++ b/spec/features/projects/members/group_members_spec.rb @@ -13,7 +13,7 @@ feature 'Projects members', feature: true do background do project.team << [developer, :developer] group.add_owner(user) - login_as(user) + gitlab_sign_in(user) end context 'with a group invitee' do diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb index bdeeef57273..66da28b07fe 100644 --- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb +++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb @@ -8,7 +8,7 @@ feature 'Projects > Members > Group requester cannot request access to project', background do group.add_owner(owner) - login_as(user) + gitlab_sign_in(user) visit group_path(group) perform_enqueued_jobs { click_link 'Request Access' } visit namespace_project_path(project.namespace, project) diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb index deea34214fb..9fdd7df0ee5 100644 --- a/spec/features/projects/members/list_spec.rb +++ b/spec/features/projects/members/list_spec.rb @@ -9,7 +9,7 @@ feature 'Project members list', feature: true do let(:project) { create(:project, namespace: group) } background do - login_as(user1) + gitlab_sign_in(user1) group.add_owner(user1) end diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 1e6f15d8258..21b48b7fdd1 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -10,7 +10,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: background do project.team << [master, :master] - login_as(master) + gitlab_sign_in(master) end scenario 'expiration date is displayed in the members list' do diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb index 143390b71cd..bd445e27243 100644 --- a/spec/features/projects/members/master_manages_access_requests_spec.rb +++ b/spec/features/projects/members/master_manages_access_requests_spec.rb @@ -8,7 +8,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do background do project.request_access(user) project.team << [master, :master] - login_as(master) + gitlab_sign_in(master) end scenario 'master can see access requests' do diff --git a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb index 9564347e733..703f5dff6b5 100644 --- a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb +++ b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb @@ -6,7 +6,7 @@ feature 'Projects > Members > Member cannot request access to his project', feat background do project.team << [member, :developer] - login_as(member) + gitlab_sign_in(member) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index 5daa932e4e6..8e1788f7f2a 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -6,7 +6,7 @@ feature 'Projects > Members > Member leaves project', feature: true do background do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb index b26d55c5d5d..70e4bb19c0f 100644 --- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb +++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb @@ -4,7 +4,7 @@ feature 'Projects > Members > Owner cannot leave project', feature: true do let(:project) { create(:project) } background do - login_as(project.owner) + gitlab_sign_in(project.owner) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb index 4ca9272b9c1..0cd7e3afeda 100644 --- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb +++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb @@ -4,7 +4,7 @@ feature 'Projects > Members > Owner cannot request access to his project', featu let(:project) { create(:project) } background do - login_as(project.owner) + gitlab_sign_in(project.owner) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index d428f6fcf22..66d98ef8b90 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -8,7 +8,7 @@ feature 'Projects > Members > Sorting', feature: true do background do create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) - login_as(master) + gitlab_sign_in(master) end scenario 'sorts alphabetically by default' do diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index ec48a4bd726..081009f2325 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -6,7 +6,7 @@ feature 'Projects > Members > User requests access', feature: true do let(:master) { project.owner } background do - login_as(user) + gitlab_sign_in(user) visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 1370ab1c521..6de8855016d 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -18,7 +18,7 @@ feature 'Merge Request button', feature: true do context 'logged in as developer' do before do - login_as(user) + gitlab_sign_in(user) project.team << [user, :developer] end @@ -52,7 +52,7 @@ feature 'Merge Request button', feature: true do context 'logged in as non-member' do before do - login_as(user) + gitlab_sign_in(user) end it 'does not show Create merge request button' do diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb index 7e8a796c55d..f2a2fd0311f 100644 --- a/spec/features/projects/merge_requests/list_spec.rb +++ b/spec/features/projects/merge_requests/list_spec.rb @@ -7,7 +7,7 @@ feature 'Merge Requests List' do background do project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) end scenario 'user does not see create new list button' do diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb index b4fc0edbde8..a02e4118784 100644 --- a/spec/features/projects/milestones/milestone_spec.rb +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -6,7 +6,7 @@ feature 'Project milestone', :feature do let(:milestone) { create(:milestone, project: project) } before do - login_as(user) + gitlab_sign_in(user) end context 'when project has enabled issues' do diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb index da3eaed707a..2350089255d 100644 --- a/spec/features/projects/milestones/milestones_sorting_spec.rb +++ b/spec/features/projects/milestones/milestones_sorting_spec.rb @@ -15,7 +15,7 @@ feature 'Milestones sorting', :feature, :js do due_date: 11.days.from_now, created_at: 1.hour.ago, title: "bbb", project: project) - login_as(user) + gitlab_sign_in(user) end scenario 'visit project milestones and sort by due_date_asc' do diff --git a/spec/features/projects/milestones/new_spec.rb b/spec/features/projects/milestones/new_spec.rb new file mode 100644 index 00000000000..7403822c7fb --- /dev/null +++ b/spec/features/projects/milestones/new_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +feature 'Creating a new project milestone', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) } + + before do + login_as(user) + visit new_namespace_project_milestone_path(project.namespace, project) + end + + it 'description has autocomplete' do + find('#milestone_description').native.send_keys('') + fill_in 'milestone_description', with: '@' + + expect(page).to have_selector('.atwho-view') + end +end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index b1f9eb15667..37d9a97033b 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -4,7 +4,7 @@ feature "New project", feature: true do let(:user) { create(:admin) } before do - login_as(user) + gitlab_sign_in(user) end context "Visibility level selector" do diff --git a/spec/features/projects/no_password_spec.rb b/spec/features/projects/no_password_spec.rb new file mode 100644 index 00000000000..30a16e38e3c --- /dev/null +++ b/spec/features/projects/no_password_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +feature 'No Password Alert' do + let(:project) { create(:project, namespace: user.namespace) } + + context 'with internal auth enabled' do + before do + sign_in(user) + visit namespace_project_path(project.namespace, project) + end + + context 'when user has a password' do + let(:user) { create(:user) } + + it 'shows no alert' do + expect(page).not_to have_content "You won't be able to pull or push project code via HTTP until you set a password on your account" + end + end + + context 'when user has password automatically set' do + let(:user) { create(:user, password_automatically_set: true) } + + it 'shows a password alert' do + expect(page).to have_content "You won't be able to pull or push project code via HTTP until you set a password on your account" + end + end + end + + context 'with internal auth disabled' do + let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') } + + before do + stub_application_setting(signin_enabled?: false) + stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config]) + end + + context 'when user has no personal access tokens' do + it 'has a personal access token alert' do + gitlab_sign_in_via('saml', user, 'my-uid') + visit namespace_project_path(project.namespace, project) + + expect(page).to have_content "You won't be able to pull or push project code via HTTP until you create a personal access token on your account" + end + end + + context 'when user has a personal access token' do + it 'shows no alert' do + create(:personal_access_token, user: user) + gitlab_sign_in_via('saml', user, 'my-uid') + visit namespace_project_path(project.namespace, project) + + expect(page).not_to have_content "You won't be able to pull or push project code via HTTP until you create a personal access token on your account" + end + end + end + + context 'when user is ldap user' do + let(:user) { create(:omniauth_user, password_automatically_set: true) } + + before do + sign_in(user) + visit namespace_project_path(project.namespace, project) + end + + it 'shows no alert' do + expect(page).not_to have_content "You won't be able to pull or push project code via HTTP until you" + end + end +end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 11793c0f303..e9a3cfb7f60 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -10,7 +10,7 @@ feature 'Pages', feature: true do project.team << [user, role] - login_as(user) + gitlab_sign_in(user) end shared_examples 'no pages deployed' do diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 2d43f7a10bc..dfb973c37e5 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -12,7 +12,7 @@ feature 'Pipeline Schedules', :feature do before do project.add_master(user) - login_as(user) + gitlab_sign_in(user) visit_page end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 36a3ddca6ef..e182995922d 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -7,7 +7,7 @@ describe 'Pipeline', :feature, :js do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) project.team << [user, :developer] end @@ -47,7 +47,9 @@ describe 'Pipeline', :feature, :js do let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) } - before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + before do + visit namespace_project_pipeline_path(project.namespace, project, pipeline) + end it 'shows the pipeline graph' do expect(page).to have_selector('.pipeline-visualization') @@ -164,7 +166,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_content('retried') } context 'when retrying' do - before { find('.js-retry-button').trigger('click') } + before do + find('.js-retry-button').trigger('click') + end it { expect(page).not_to have_content('Retry') } end @@ -174,7 +178,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do - before { click_on 'Cancel running' } + before do + click_on 'Cancel running' + end it { expect(page).not_to have_content('Cancel running') } end @@ -226,7 +232,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_content('retried') } context 'when retrying' do - before { find('.js-retry-button').trigger('click') } + before do + find('.js-retry-button').trigger('click') + end it { expect(page).not_to have_content('Retry') } end @@ -236,7 +244,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do - before { click_on 'Cancel running' } + before do + click_on 'Cancel running' + end it { expect(page).not_to have_content('Cancel running') } end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 05c2bf350f1..d36d073e022 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -7,7 +7,7 @@ describe 'Pipelines', :feature, :js do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) project.team << [user, :developer] end @@ -149,7 +149,9 @@ describe 'Pipelines', :feature, :js do create(:ci_pipeline, :invalid, project: project) end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'contains badge that indicates errors' do expect(page).to have_content 'yaml invalid' @@ -171,10 +173,12 @@ describe 'Pipelines', :feature, :js do commands: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'has a dropdown with play button' do - expect(page).to have_selector('.dropdown-toggle.btn.btn-default .icon-play') + expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play') end it 'has link to the manual action' do @@ -204,7 +208,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'is cancelable' do expect(page).to have_selector('.js-pipelines-cancel-button') @@ -215,7 +221,9 @@ describe 'Pipelines', :feature, :js do end context 'when canceling' do - before { find('.js-pipelines-cancel-button').trigger('click') } + before do + find('.js-pipelines-cancel-button').trigger('click') + end it 'indicates that pipeline was canceled' do expect(page).not_to have_selector('.js-pipelines-cancel-button') @@ -255,7 +263,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'has artifats' do expect(page).to have_selector('.build-artifacts') @@ -284,7 +294,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it { expect(page).not_to have_selector('.build-artifacts') } end @@ -297,7 +309,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it { expect(page).not_to have_selector('.build-artifacts') } end @@ -310,7 +324,9 @@ describe 'Pipelines', :feature, :js do name: 'build') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'should render a mini pipeline graph' do expect(page).to have_selector('.js-mini-pipeline-graph') @@ -437,7 +453,9 @@ describe 'Pipelines', :feature, :js do end context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } + before do + stub_ci_pipeline_to_return_yaml_file + end it 'creates a new pipeline' do expect { click_on 'Create pipeline' } @@ -448,7 +466,9 @@ describe 'Pipelines', :feature, :js do end context 'without gitlab-ci.yml' do - before { click_on 'Create pipeline' } + before do + click_on 'Create pipeline' + end it { expect(page).to have_content('Missing .gitlab-ci.yml file') } end diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 11dcab4d737..baa38ff8cca 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -7,7 +7,7 @@ describe 'Edit Project Settings', feature: true do let(:project) { create(:empty_project, namespace: user.namespace, path: 'gitlab', name: 'sample') } before do - login_as(user) + gitlab_sign_in(user) end describe 'Project settings section', js: true do @@ -58,8 +58,13 @@ describe 'Edit Project Settings', feature: true do # Not using empty project because we need a repo to exist let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') } - before(:context) { TestEnv.clean_test_path } - after(:example) { TestEnv.clean_test_path } + before(:context) do + TestEnv.clean_test_path + end + + after(:example) do + TestEnv.clean_test_path + end specify 'the project is accessible via the new path' do rename_project(project, path: 'bar') @@ -96,9 +101,17 @@ describe 'Edit Project Settings', feature: true do let!(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') } let!(:group) { create(:group) } - before(:context) { TestEnv.clean_test_path } - before(:example) { group.add_owner(user) } - after(:example) { TestEnv.clean_test_path } + before(:context) do + TestEnv.clean_test_path + end + + before(:example) do + group.add_owner(user) + end + + after(:example) do + TestEnv.clean_test_path + end specify 'the project is accessible via the new path' do transfer_project(project, group) diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 04414490571..016a992bdcf 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -6,7 +6,7 @@ feature 'Ref switcher', feature: true, js: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_tree_path(project.namespace, project, 'master') end diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb index c96d87e5708..8cd216c8fdb 100644 --- a/spec/features/projects/services/jira_service_spec.rb +++ b/spec/features/projects/services/jira_service_spec.rb @@ -6,7 +6,11 @@ feature 'Setup Jira service', :feature, :js do let(:service) { project.create_jira_service } let(:url) { 'http://jira.example.com' } - let(:project_url) { 'http://username:password@jira.example.com/rest/api/2/project/GitLabProject' } + + def stub_project_url + WebMock.stub_request(:get, 'http://jira.example.com/rest/api/2/project/GitLabProject') + .with(basic_auth: %w(username password)) + end def fill_form(active = true) check 'Active' if active @@ -20,7 +24,7 @@ feature 'Setup Jira service', :feature, :js do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_settings_integrations_path(project.namespace, project) end @@ -28,7 +32,7 @@ feature 'Setup Jira service', :feature, :js do describe 'user sets and activates Jira Service' do context 'when Jira connection test succeeds' do before do - WebMock.stub_request(:get, project_url) + stub_project_url end it 'activates the JIRA service' do @@ -44,7 +48,7 @@ feature 'Setup Jira service', :feature, :js do context 'when Jira connection test fails' do before do - WebMock.stub_request(:get, project_url).to_return(status: 401) + stub_project_url.to_return(status: 401) end it 'shows errors when some required fields are not filled in' do diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 1fe82222e59..d87985f1c92 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -9,7 +9,7 @@ feature 'Setup Mattermost slash commands', :feature, :js do before do stub_mattermost_setting(enabled: mattermost_enabled) project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit edit_namespace_project_service_path(project.namespace, project, service) end diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index c0a4a1e4bf5..50707e6a49f 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -9,7 +9,7 @@ feature 'Projects > Slack service > Setup events', feature: true do service.fields service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7) project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end scenario 'user can filter events by channel' do diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb index f53b820c460..3fae38c1799 100644 --- a/spec/features/projects/services/slack_slash_command_spec.rb +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -7,7 +7,7 @@ feature 'Slack slash commands', feature: true do background do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit edit_namespace_project_service_path(project.namespace, project, service) end diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb index fbaea14a2be..a59374b37ea 100644 --- a/spec/features/projects/settings/integration_settings_spec.rb +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -7,7 +7,7 @@ feature 'Integration settings', feature: true do let(:integrations_path) { namespace_project_settings_integrations_path(project.namespace, project) } background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 321af416c91..f2af14ceab2 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -8,7 +8,7 @@ feature 'Project settings > Merge Requests', feature: true, js: true do background do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'when Merge Request and Pipelines are initially enabled' do diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 035c57eaa47..c33fbd49d21 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -8,7 +8,7 @@ feature "Pipelines settings", feature: true do let(:role) { :developer } background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] visit namespace_project_pipelines_settings_path(project.namespace, project) end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 4cc38c5286e..35cd0d6e832 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -7,7 +7,7 @@ feature 'Repository settings', feature: true do background do project.team << [user, role] - login_as(user) + gitlab_sign_in(user) end context 'for developer' do @@ -65,6 +65,23 @@ feature 'Repository settings', feature: true do expect(page).to have_content('Write access allowed') end + scenario 'edit a deploy key from projects user has access to' do + project2 = create(:project_empty_repo) + project2.team << [user, role] + project2.deploy_keys << private_deploy_key + + visit namespace_project_settings_repository_path(project.namespace, project) + + find('li', text: private_deploy_key.title).click_link('Edit') + + fill_in 'deploy_key_title', with: 'updated_deploy_key' + check 'deploy_key_can_push' + click_button 'Save changes' + + expect(page).to have_content('updated_deploy_key') + expect(page).to have_content('Write access allowed') + end + scenario 'remove an existing deploy key' do project.deploy_keys << private_deploy_key visit namespace_project_settings_repository_path(project.namespace, project) diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index fac4506bdf6..18c71dee41b 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -6,7 +6,7 @@ feature 'Visibility settings', feature: true, js: true do context 'as owner' do before do - login_as(user) + gitlab_sign_in(user) visit edit_namespace_project_path(project.namespace, project) end @@ -32,7 +32,7 @@ feature 'Visibility settings', feature: true, js: true do before do project.team << [master_user, :master] - login_as(master_user) + gitlab_sign_in(master_user) visit edit_namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb index 54aa9c66a08..cec79277c33 100644 --- a/spec/features/projects/shortcuts_spec.rb +++ b/spec/features/projects/shortcuts_spec.rb @@ -7,7 +7,7 @@ feature 'Project shortcuts', feature: true do describe 'On a project', js: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb index 5ac1ca45c74..c75d6dbc307 100644 --- a/spec/features/projects/snippets/create_snippet_spec.rb +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -17,7 +17,7 @@ feature 'Create Snippet', :js, feature: true do context 'when a user is authenticated' do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_snippets_path(project.namespace, project) diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb index b844e60e5d5..9e73ba4123b 100644 --- a/spec/features/projects/snippets/show_spec.rb +++ b/spec/features/projects/snippets/show_spec.rb @@ -7,7 +7,7 @@ feature 'Project snippet', :js, feature: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'Ruby file' do diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb index 18689c17fe9..80dbffaffc7 100644 --- a/spec/features/projects/snippets_spec.rb +++ b/spec/features/projects/snippets_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Project snippets', feature: true do +describe 'Project snippets', :js, feature: true do context 'when the project has snippets' do let(:project) { create(:empty_project, :public) } let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) } @@ -26,5 +26,19 @@ describe 'Project snippets', feature: true do expect(page).to have_content(snippets[1].title) end end + + context 'when submitting a note' do + before do + gitlab_sign_in :admin + visit namespace_project_snippet_path(project.namespace, project, snippets[0]) + end + + it 'should have autocomplete' do + find('#note_note').native.send_keys('') + fill_in 'note[note]', with: '@' + + expect(page).to have_selector('.atwho-view') + end + end end end diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb index e88907b8016..63eb97d5a92 100644 --- a/spec/features/projects/sub_group_issuables_spec.rb +++ b/spec/features/projects/sub_group_issuables_spec.rb @@ -8,7 +8,7 @@ describe 'Subgroup Issuables', :feature, :js, :nested_groups do before do project.add_master(user) - login_as user + gitlab_sign_in user end it 'shows the full subgroup title when issues index page is empty' do diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb index dd93d25c2c6..ca00a51aa3c 100644 --- a/spec/features/projects/tags/download_buttons_spec.rb +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -23,7 +23,7 @@ feature 'Download buttons in tags page', feature: true do end background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] end diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb index 9bf59c4139c..135584e5bf8 100644 --- a/spec/features/projects/tree/rss_spec.rb +++ b/spec/features/projects/tree/rss_spec.rb @@ -8,7 +8,7 @@ feature 'Project Tree RSS' do before do user = create(:user) project.team << [user, :developer] - login_as(user) + gitlab_sign_in(user) visit path end diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb index aeb7e0b7c33..f375e1215db 100644 --- a/spec/features/projects/user_create_dir_spec.rb +++ b/spec/features/projects/user_create_dir_spec.rb @@ -6,7 +6,7 @@ feature 'New directory creation', feature: true, js: true do given(:project) { create(:project) } background do - login_as(user) + gitlab_sign_in(user) project.team << [user, role] visit namespace_project_tree_path(project.namespace, project, 'master') open_new_directory_modal diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb new file mode 100644 index 00000000000..29f1eb8d73e --- /dev/null +++ b/spec/features/projects/user_creates_project_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +feature 'User creates a project', js: true do + let(:user) { create(:user) } + + before do + sign_in(user) + create(:personal_key, user: user) + visit(new_project_path) + end + + it 'creates a new project' do + fill_in(:project_path, with: 'Empty') + + page.within('#content-body') do + click_button('Create project') + end + + project = Project.last + + expect(current_path).to eq(namespace_project_path(project.namespace, project)) + expect(page).to have_content('Empty') + expect(page).to have_content('git init') + expect(page).to have_content('git remote') + expect(page).to have_content(project.url_to_repo) + end +end diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index 640f1376548..f6a640b90b4 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -50,7 +50,7 @@ describe 'View on environment', js: true do let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) } before do - login_as(user) + gitlab_sign_in(user) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) @@ -66,7 +66,7 @@ describe 'View on environment', js: true do context 'when visiting a comparison for the branch' do before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name) @@ -80,7 +80,7 @@ describe 'View on environment', js: true do context 'when visiting a comparison for the commit' do before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha) @@ -94,7 +94,7 @@ describe 'View on environment', js: true do context 'when visiting a blob on the branch' do before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path)) @@ -108,7 +108,7 @@ describe 'View on environment', js: true do context 'when visiting a blob on the commit' do before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path)) @@ -122,7 +122,7 @@ describe 'View on environment', js: true do context 'when visiting the commit' do before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_commit_path(project.namespace, project, sha) diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 94f6bb16730..fd6c09943e3 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -16,7 +16,7 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t project.team << [user, :master] WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute - login_as(user) + gitlab_sign_in(user) visit namespace_project_path(project.namespace, project) find('.shortcuts-wiki').trigger('click') diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb index c1f6b0cce3b..ab0ed9b8204 100644 --- a/spec/features/projects/wiki/shortcuts_spec.rb +++ b/spec/features/projects/wiki/shortcuts_spec.rb @@ -8,7 +8,7 @@ feature 'Wiki shortcuts', :feature, :js do end before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_wiki_path(project.namespace, project, wiki_page) end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 8912d575878..a477dcf7ee9 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -5,7 +5,7 @@ feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do background do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) visit namespace_project_path(project.namespace, project) find('.shortcuts-wiki').trigger('click') @@ -133,6 +133,22 @@ feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do expect(page).to have_content('My awesome wiki!') end end + + scenario 'content has autocomplete', :js do + click_link 'New page' + + page.within '#modal-new-wiki' do + fill_in :new_wiki_path, with: 'test-autocomplete' + click_button 'Create page' + end + + page.within '.wiki-form' do + find('#wiki_content').native.send_keys('') + fill_in :wiki_content, with: '@' + end + + expect(page).to have_selector('.atwho-view') + end end end diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb index 95826e7e5be..7d31122af35 100644 --- a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb @@ -13,7 +13,7 @@ describe 'Projects > Wiki > User views Git access wiki page', :feature do end before do - login_as(user) + gitlab_sign_in(user) end scenario 'Visit Wiki Page Current Commit' do diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 86cf520ea80..64a30438681 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -5,11 +5,10 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do background do project.team << [user, :master] - login_as(user) - - visit namespace_project_path(project.namespace, project) WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute - click_link 'Wiki' + gitlab_sign_in(user) + + visit namespace_project_wikis_path(project.namespace, project) end context 'in the user namespace' do @@ -42,6 +41,15 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do expect(page).to have_content('Content can\'t be blank') expect(find('textarea#wiki_content').value).to eq '' end + + scenario 'content has autocomplete', :js do + click_link 'Edit' + + find('#wiki_content').native.send_keys('') + fill_in :wiki_content, with: '@' + + expect(page).to have_selector('.atwho-view') + end end end diff --git a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb index c17e06612de..8a88ab247f3 100644 --- a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb @@ -15,7 +15,7 @@ feature 'Projects > Wiki > User views the wiki page', feature: true do background do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) WikiPages::UpdateService.new( project, user, diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb index 20219f3cc9a..36799925167 100644 --- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -5,7 +5,7 @@ describe 'Projects > Wiki > User views wiki in project page', feature: true do before do project.team << [user, :master] - login_as(user) + gitlab_sign_in(user) end context 'when repository is disabled for project' do diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 060e19596ae..7e8a703db93 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -6,7 +6,7 @@ feature 'Project', feature: true do let(:path) { namespace_project_path(project.namespace, project) } before do - login_as(:admin) + gitlab_sign_in(:admin) end it 'parses Markdown' do @@ -39,7 +39,7 @@ feature 'Project', feature: true do let(:project) { create(:empty_project, namespace: user.namespace) } before do - login_with user + gitlab_sign_in user create(:forked_project_link, forked_to_project: project) visit edit_namespace_project_path(project.namespace, project) end @@ -60,7 +60,7 @@ feature 'Project', feature: true do let(:project) { create(:empty_project, namespace: user.namespace, name: 'project1') } before do - login_with(user) + gitlab_sign_in(user) project.team << [user, :master] visit edit_namespace_project_path(project.namespace, project) end @@ -79,7 +79,7 @@ feature 'Project', feature: true do let(:project) { create(:empty_project, namespace: user.namespace) } before do - login_with(user) + gitlab_sign_in(user) project.add_user(user, Gitlab::Access::MASTER) visit namespace_project_path(project.namespace, project) end @@ -98,7 +98,7 @@ feature 'Project', feature: true do context 'on issues page', js: true do before do - login_with(user) + gitlab_sign_in(user) project.add_user(user, Gitlab::Access::MASTER) project2.add_user(user, Gitlab::Access::MASTER) visit namespace_project_issue_path(project.namespace, project, issue) @@ -123,7 +123,7 @@ feature 'Project', feature: true do before do project.team << [user, :master] - login_as user + gitlab_sign_in user visit namespace_project_path(project.namespace, project) end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 667895bffa5..20b8e10f0f7 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -4,7 +4,9 @@ feature 'Protected Branches', feature: true, js: true do let(:user) { create(:user, :admin) } let(:project) { create(:project, :repository) } - before { login_as(user) } + before do + gitlab_sign_in(user) + end def set_protected_branch_name(branch_name) find(".js-protected-branch-select").trigger('click') diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb index 66236dbc7fc..73a80692154 100644 --- a/spec/features/protected_tags_spec.rb +++ b/spec/features/protected_tags_spec.rb @@ -4,7 +4,9 @@ feature 'Projected Tags', feature: true, js: true do let(:user) { create(:user, :admin) } let(:project) { create(:project, :repository) } - before { login_as(user) } + before do + gitlab_sign_in(user) + end def set_protected_tag_name(tag_name) find(".js-protected-tag-select").click diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb index 39b1c4acf52..12049822753 100644 --- a/spec/features/reportable_note/commit_spec.rb +++ b/spec/features/reportable_note/commit_spec.rb @@ -8,7 +8,7 @@ describe 'Reportable note on commit', :feature, :js do before do project.add_master(user) - login_as user + gitlab_sign_in(user) end context 'a normal note' do diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb index 5f526818994..ca2a7f41496 100644 --- a/spec/features/reportable_note/issue_spec.rb +++ b/spec/features/reportable_note/issue_spec.rb @@ -8,7 +8,7 @@ describe 'Reportable note on issue', :feature, :js do before do project.add_master(user) - login_as user + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb index 6d053d26626..8e75b4af3eb 100644 --- a/spec/features/reportable_note/merge_request_spec.rb +++ b/spec/features/reportable_note/merge_request_spec.rb @@ -7,7 +7,7 @@ describe 'Reportable note on merge request', :feature, :js do before do project.add_master(user) - login_as user + gitlab_sign_in(user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb index 3f1e0cf9097..5bee4a31379 100644 --- a/spec/features/reportable_note/snippets_spec.rb +++ b/spec/features/reportable_note/snippets_spec.rb @@ -6,7 +6,7 @@ describe 'Reportable note on snippets', :feature, :js do before do project.add_master(user) - login_as user + gitlab_sign_in(user) end describe 'on project snippet' do @@ -19,15 +19,4 @@ describe 'Reportable note on snippets', :feature, :js do it_behaves_like 'reportable note' end - - describe 'on personal snippet' do - let(:snippet) { create(:personal_snippet, :public, author: user) } - let!(:note) { create(:note_on_personal_snippet, noteable: snippet, author: user) } - - before do - visit snippet_path(snippet) - end - - it_behaves_like 'reportable note' - end end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 0e1cc9a0f73..ea18879b4bf 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -4,7 +4,10 @@ describe "Runners" do include GitlabRoutingHelper let(:user) { create(:user) } - before { login_as(user) } + + before do + gitlab_sign_in(user) + end describe "specific runners" do before do @@ -127,7 +130,9 @@ describe "Runners" do end context 'when runner has tags' do - before { runner.update_attribute(:tag_list, ['tag']) } + before do + runner.update_attribute(:tag_list, ['tag']) + end scenario 'user wants to prevent runner from running untagged job' do visit runners_path(project) diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 7834807b1f1..64469f999af 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -9,7 +9,7 @@ describe "Search", feature: true do let!(:issue2) { create(:issue, project: project, author: user) } before do - login_with(user) + gitlab_sign_in(user) project.team << [user, :reporter] visit search_path end @@ -83,7 +83,9 @@ describe "Search", feature: true do let(:project) { create(:project, :repository) } let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') } - before { note.update_attributes(commit_id: 12345678) } + before do + note.update_attributes(commit_id: 12345678) + end it 'finds comment' do visit namespace_project_path(project.namespace, project) diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 2a2655bbdb5..f33406a40a7 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -337,7 +337,9 @@ describe "Internal Project Access", feature: true do subject { namespace_project_jobs_path(project.namespace, project) } context "when allowed for public and internal" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -351,7 +353,9 @@ describe "Internal Project Access", feature: true do end context "when disallowed for public and internal" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -371,7 +375,9 @@ describe "Internal Project Access", feature: true do subject { namespace_project_job_path(project.namespace, project, build.id) } context "when allowed for public and internal" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -385,7 +391,9 @@ describe "Internal Project Access", feature: true do end context "when disallowed for public and internal" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 35d5163941e..16a1331b2f3 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -157,7 +157,9 @@ describe "Public Project Access", feature: true do subject { namespace_project_jobs_path(project.namespace, project) } context "when allowed for public" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -171,7 +173,9 @@ describe "Public Project Access", feature: true do end context "when disallowed for public" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -191,7 +195,9 @@ describe "Public Project Access", feature: true do subject { namespace_project_job_path(project.namespace, project, build.id) } context "when allowed for public" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -205,7 +211,9 @@ describe "Public Project Access", feature: true do end context "when disallowed for public" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index d7b6dda4946..5d6d1e79af2 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do context "when sending confirmation email" do - before { stub_application_setting(send_user_confirmation_email: true) } + before do + stub_application_setting(send_user_confirmation_email: true) + end it 'creates the user account and sends a confirmation email' do user = build(:user) @@ -23,7 +25,9 @@ feature 'Signup', feature: true do end context "when not sending confirmation email" do - before { stub_application_setting(send_user_confirmation_email: false) } + before do + stub_application_setting(send_user_confirmation_email: false) + end it 'creates the user account and goes to dashboard' do user = build(:user) diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index ddd31ede064..ac5c14ed427 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -4,7 +4,7 @@ feature 'Create Snippet', :js, feature: true do include DropzoneHelper before do - login_as :user + gitlab_sign_in :user visit new_snippet_path end diff --git a/spec/features/snippets/edit_snippet_spec.rb b/spec/features/snippets/edit_snippet_spec.rb index 89ae593db88..860e1b156d6 100644 --- a/spec/features/snippets/edit_snippet_spec.rb +++ b/spec/features/snippets/edit_snippet_spec.rb @@ -10,7 +10,7 @@ feature 'Edit Snippet', :js, feature: true do let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) } before do - login_as(user) + gitlab_sign_in(user) visit edit_snippet_path(snippet) wait_for_requests diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb index fd097fe2e74..ec75817b942 100644 --- a/spec/features/snippets/explore_spec.rb +++ b/spec/features/snippets/explore_spec.rb @@ -6,7 +6,7 @@ feature 'Explore Snippets', feature: true do let!(:private_snippet) { create(:personal_snippet, :private) } scenario 'User should see snippets that are not private' do - login_as create(:user) + gitlab_sign_in create(:user) visit explore_snippets_path expect(page).to have_content(public_snippet.title) @@ -15,7 +15,7 @@ feature 'Explore Snippets', feature: true do end scenario 'External user should see only public snippets' do - login_as create(:user, :external) + gitlab_sign_in create(:user, :external) visit explore_snippets_path expect(page).to have_content(public_snippet.title) diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb index 93382f4c359..3babb1c02cc 100644 --- a/spec/features/snippets/internal_snippet_spec.rb +++ b/spec/features/snippets/internal_snippet_spec.rb @@ -5,7 +5,7 @@ feature 'Internal Snippets', feature: true, js: true do describe 'normal user' do before do - login_as :user + gitlab_sign_in :user end scenario 'sees internal snippets' do diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index 44b0c89fac7..c7e2e3d8a34 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -14,7 +14,7 @@ describe 'Comments on personal snippets', :js, feature: true do let!(:other_note) { create(:note_on_personal_snippet) } before do - login_as user + gitlab_sign_in user visit snippet_path(snippet) end @@ -33,6 +33,7 @@ describe 'Comments on personal snippets', :js, feature: true do expect(page).to have_selector('.note-emoji-button') end + find('body').click # close dropdown open_more_actions_dropdown(snippet_notes[1]) page.within("#notes-list li#note_#{snippet_notes[1].id}") do @@ -46,8 +47,8 @@ describe 'Comments on personal snippets', :js, feature: true do context 'when submitting a note' do it 'shows a valid form' do is_expected.to have_css('.js-main-target-form', visible: true, count: 1) - expect(find('.js-main-target-form .js-comment-button').value). - to eq('Comment') + expect(find('.js-main-target-form .js-comment-button').value) + .to eq('Comment') page.within('.js-main-target-form') do expect(page).not_to have_link('Cancel') @@ -70,6 +71,22 @@ describe 'Comments on personal snippets', :js, feature: true do expect(find('div#notes')).to have_content('This is awesome!') end + + it 'should not have autocomplete' do + wait_for_requests + request_count_before = page.driver.network_traffic.count + + find('#note_note').native.send_keys('') + fill_in 'note[note]', with: '@' + + wait_for_requests + request_count_after = page.driver.network_traffic.count + + # This selector probably won't be in place even if autocomplete was enabled + # but we want to make sure + expect(page).not_to have_selector('.atwho-view') + expect(request_count_before).to eq(request_count_after) + end end context 'when editing a note' do diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb index 146cd3af848..4c21e7321f4 100644 --- a/spec/features/snippets/search_snippets_spec.rb +++ b/spec/features/snippets/search_snippets_spec.rb @@ -5,7 +5,7 @@ feature 'Search Snippets', feature: true do public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle') private_snippet = create(:personal_snippet, :private, title: 'Middle and End') - login_as private_snippet.author + gitlab_sign_in private_snippet.author visit dashboard_snippets_path page.within '.search' do @@ -41,7 +41,7 @@ feature 'Search Snippets', feature: true do CONTENT ) - login_as create(:user) + gitlab_sign_in create(:user) visit dashboard_snippets_path page.within '.search' do diff --git a/spec/features/snippets/user_snippets_spec.rb b/spec/features/snippets/user_snippets_spec.rb index 191c2fb9a22..b971c6aab53 100644 --- a/spec/features/snippets/user_snippets_spec.rb +++ b/spec/features/snippets/user_snippets_spec.rb @@ -7,7 +7,7 @@ feature 'User Snippets', feature: true do let!(:private_snippet) { create(:personal_snippet, :private, author: author, title: "This is a private snippet") } background do - login_as author + gitlab_sign_in author visit dashboard_snippets_path end diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb index af25eebed13..52db3583dac 100644 --- a/spec/features/tags/master_creates_tag_spec.rb +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -6,62 +6,80 @@ feature 'Master creates tag', feature: true do before do project.team << [user, :master] - login_with(user) - visit namespace_project_tags_path(project.namespace, project) + gitlab_sign_in(user) end - scenario 'with an invalid name displays an error' do - create_tag_in_form(tag: 'v 1.0', ref: 'master') + context 'from tag list' do + before do + visit namespace_project_tags_path(project.namespace, project) + end - expect(page).to have_content 'Tag name invalid' - end + scenario 'with an invalid name displays an error' do + create_tag_in_form(tag: 'v 1.0', ref: 'master') - scenario 'with an invalid reference displays an error' do - create_tag_in_form(tag: 'v2.0', ref: 'foo') + expect(page).to have_content 'Tag name invalid' + end - expect(page).to have_content 'Target foo is invalid' - end + scenario 'with an invalid reference displays an error' do + create_tag_in_form(tag: 'v2.0', ref: 'foo') - scenario 'that already exists displays an error' do - create_tag_in_form(tag: 'v1.1.0', ref: 'master') + expect(page).to have_content 'Target foo is invalid' + end - expect(page).to have_content 'Tag v1.1.0 already exists' - end + scenario 'that already exists displays an error' do + create_tag_in_form(tag: 'v1.1.0', ref: 'master') + + expect(page).to have_content 'Tag v1.1.0 already exists' + end - scenario 'with multiline message displays the message in a <pre> block' do - create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world") + scenario 'with multiline message displays the message in a <pre> block' do + create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world") - expect(current_path).to eq( - namespace_project_tag_path(project.namespace, project, 'v3.0')) - expect(page).to have_content 'v3.0' - page.within 'pre.wrap' do - expect(page).to have_content "Awesome tag message\n\n- hello\n- world" + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v3.0')) + expect(page).to have_content 'v3.0' + page.within 'pre.wrap' do + expect(page).to have_content "Awesome tag message\n\n- hello\n- world" + end end - end - scenario 'with multiline release notes parses the release note as Markdown' do - create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world") + scenario 'with multiline release notes parses the release note as Markdown' do + create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world") - expect(current_path).to eq( - namespace_project_tag_path(project.namespace, project, 'v4.0')) - expect(page).to have_content 'v4.0' - page.within '.description' do - expect(page).to have_content 'Awesome release notes' - expect(page).to have_selector('ul li', count: 2) + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v4.0')) + expect(page).to have_content 'v4.0' + page.within '.description' do + expect(page).to have_content 'Awesome release notes' + expect(page).to have_selector('ul li', count: 2) + end + end + + scenario 'opens dropdown for ref', js: true do + click_link 'New tag' + ref_row = find('.form-group:nth-of-type(2) .col-sm-10') + page.within ref_row do + ref_input = find('[name="ref"]', visible: false) + expect(ref_input.value).to eq 'master' + expect(find('.dropdown-toggle-text')).to have_content 'master' + + find('.js-branch-select').trigger('click') + + expect(find('.dropdown-menu')).to have_content 'empty-branch' + end end end - scenario 'opens dropdown for ref', js: true do - click_link 'New tag' - ref_row = find('.form-group:nth-of-type(2) .col-sm-10') - page.within ref_row do - ref_input = find('[name="ref"]', visible: false) - expect(ref_input.value).to eq 'master' - expect(find('.dropdown-toggle-text')).to have_content 'master' + context 'from new tag page' do + before do + visit new_namespace_project_tag_path(project.namespace, project) + end - find('.js-branch-select').trigger('click') + it 'description has autocomplete', :js do + find('#release_description').native.send_keys('') + fill_in 'release_description', with: '@' - expect(find('.dropdown-menu')).to have_content 'empty-branch' + expect(page).to have_selector('.atwho-view') end end diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index ccfafe6db7d..58f33e954f9 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -6,7 +6,7 @@ feature 'Master deletes tag', feature: true do before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) visit namespace_project_tags_path(project.namespace, project) end diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb index 6b5b3122f72..18c8c4c511c 100644 --- a/spec/features/tags/master_updates_tag_spec.rb +++ b/spec/features/tags/master_updates_tag_spec.rb @@ -6,7 +6,7 @@ feature 'Master updates tag', feature: true do before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) visit namespace_project_tags_path(project.namespace, project) end @@ -24,6 +24,17 @@ feature 'Master updates tag', feature: true do expect(page).to have_content 'v1.1.0' expect(page).to have_content 'Awesome release notes' end + + scenario 'description has autocomplete', :js do + page.within(first('.content-list .controls')) do + click_link 'Edit release notes' + end + + find('#release_description').native.send_keys('') + fill_in 'release_description', with: '@' + + expect(page).to have_selector('.atwho-view') + end end context 'from a specific tag page' do diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb index 922ac15a2eb..3c21fa06694 100644 --- a/spec/features/tags/master_views_tags_spec.rb +++ b/spec/features/tags/master_views_tags_spec.rb @@ -5,7 +5,7 @@ feature 'Master views tags', feature: true do before do project.team << [user, :master] - login_with(user) + gitlab_sign_in(user) end context 'when project has no tags' do diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 563e65d3cc5..51b1b8e2328 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -144,7 +144,9 @@ feature 'Task Lists', feature: true do describe 'nested tasks', js: true do let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) } - before { visit_issue(project, issue) } + before do + visit_issue(project, issue) + end it 'renders' do expect(page).to have_selector('ul.task-list', count: 2) diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb deleted file mode 100644 index feb2fe8a7d1..00000000000 --- a/spec/features/todos/todos_spec.rb +++ /dev/null @@ -1,355 +0,0 @@ -require 'spec_helper' - -describe 'Dashboard Todos', feature: true do - let(:user) { create(:user) } - let(:author) { create(:user) } - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } - let(:issue) { create(:issue, due_date: Date.today) } - - describe 'GET /dashboard/todos' do - context 'User does not have todos' do - before do - login_as(user) - visit dashboard_todos_path - end - it 'shows "All done" message' do - expect(page).to have_content "Todos let you see what you should do next." - end - end - - context 'User has a todo', js: true do - before do - create(:todo, :mentioned, user: user, project: project, target: issue, author: author) - login_as(user) - visit dashboard_todos_path - end - - it 'has todo present' do - expect(page).to have_selector('.todos-list .todo', count: 1) - end - - it 'shows due date as today' do - within first('.todo') do - expect(page).to have_content 'Due today' - end - end - - shared_examples 'deleting the todo' do - before do - within first('.todo') do - click_link 'Done' - end - end - - it 'is marked as done-reversible in the list' do - expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible') - end - - it 'shows Undo button' do - expect(page).to have_selector('.js-undo-todo', visible: true) - expect(page).to have_selector('.js-done-todo', visible: false) - end - - it 'updates todo count' do - expect(page).to have_content 'To do 0' - expect(page).to have_content 'Done 1' - end - - it 'has not "All done" message' do - expect(page).not_to have_selector('.todos-all-done') - end - end - - shared_examples 'deleting and restoring the todo' do - before do - within first('.todo') do - click_link 'Done' - wait_for_requests - click_link 'Undo' - end - end - - it 'is marked back as pending in the list' do - expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible') - expect(page).to have_selector('.todos-list .todo.todo-pending') - end - - it 'shows Done button' do - expect(page).to have_selector('.js-undo-todo', visible: false) - expect(page).to have_selector('.js-done-todo', visible: true) - end - - it 'updates todo count' do - expect(page).to have_content 'To do 1' - expect(page).to have_content 'Done 0' - end - end - - it_behaves_like 'deleting the todo' - it_behaves_like 'deleting and restoring the todo' - - context 'todo is stale on the page' do - before do - todos = TodosFinder.new(user, state: :pending).execute - TodoService.new.mark_todos_as_done(todos, user) - end - - it_behaves_like 'deleting the todo' - it_behaves_like 'deleting and restoring the todo' - end - end - - context 'User created todos for themself' do - before do - login_as(user) - end - - context 'issue assigned todo' do - before do - create(:todo, :assigned, user: user, project: project, target: issue, author: user) - visit dashboard_todos_path - end - - it 'shows issue assigned to yourself message' do - page.within('.js-todos-all') do - expect(page).to have_content("You assigned issue #{issue.to_reference(full: true)} to yourself") - end - end - end - - context 'marked todo' do - before do - create(:todo, :marked, user: user, project: project, target: issue, author: user) - visit dashboard_todos_path - end - - it 'shows you added a todo message' do - page.within('.js-todos-all') do - expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") - expect(page).not_to have_content('to yourself') - end - end - end - - context 'mentioned todo' do - before do - create(:todo, :mentioned, user: user, project: project, target: issue, author: user) - visit dashboard_todos_path - end - - it 'shows you mentioned yourself message' do - page.within('.js-todos-all') do - expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") - expect(page).not_to have_content('to yourself') - end - end - end - - context 'directly_addressed todo' do - before do - create(:todo, :directly_addressed, user: user, project: project, target: issue, author: user) - visit dashboard_todos_path - end - - it 'shows you directly addressed yourself message' do - page.within('.js-todos-all') do - expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") - expect(page).not_to have_content('to yourself') - end - end - end - - context 'approval todo' do - let(:merge_request) { create(:merge_request) } - - before do - create(:todo, :approval_required, user: user, project: project, target: merge_request, author: user) - visit dashboard_todos_path - end - - it 'shows you set yourself as an approver message' do - page.within('.js-todos-all') do - expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") - expect(page).not_to have_content('to yourself') - end - end - end - end - - context 'User has done todos', js: true do - before do - create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author) - login_as(user) - visit dashboard_todos_path(state: :done) - end - - it 'has the done todo present' do - expect(page).to have_selector('.todos-list .todo.todo-done', count: 1) - end - - describe 'restoring the todo' do - before do - within first('.todo') do - click_link 'Add todo' - end - end - - it 'is removed from the list' do - expect(page).not_to have_selector('.todos-list .todo.todo-done') - end - - it 'updates todo count' do - expect(page).to have_content 'To do 1' - expect(page).to have_content 'Done 0' - end - end - end - - context 'User has Todos with labels spanning multiple projects' do - before do - label1 = create(:label, project: project) - note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project) - create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id) - - project2 = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) - label2 = create(:label, project: project2) - issue2 = create(:issue, project: project2) - note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2) - create(:todo, :mentioned, project: project2, target: issue2, user: user, note_id: note2.id) - - login_as(user) - visit dashboard_todos_path - end - - it 'shows page with two Todos' do - expect(page).to have_selector('.todos-list .todo', count: 2) - end - end - - context 'User has multiple pages of Todos' do - before do - allow(Todo).to receive(:default_per_page).and_return(1) - - # Create just enough records to cause us to paginate - create_list(:todo, 2, :mentioned, user: user, project: project, target: issue, author: author) - - login_as(user) - end - - it 'is paginated' do - visit dashboard_todos_path - - expect(page).to have_selector('.gl-pagination') - end - - it 'is has the right number of pages' do - visit dashboard_todos_path - - expect(page).to have_selector('.gl-pagination .page', count: 2) - end - - describe 'mark all as done', js: true do - before do - visit dashboard_todos_path - find('.js-todos-mark-all').trigger('click') - end - - it 'shows "All done" message!' do - expect(page).to have_content 'To do 0' - expect(page).to have_content "You're all done!" - expect(page).not_to have_selector('.gl-pagination') - end - - it 'shows "Undo mark all as done" button' do - expect(page).to have_selector('.js-todos-mark-all', visible: false) - expect(page).to have_selector('.js-todos-undo-all', visible: true) - end - end - - describe 'undo mark all as done', js: true do - before do - visit dashboard_todos_path - end - - it 'shows the restored todo list' do - mark_all_and_undo - - expect(page).to have_selector('.todos-list .todo', count: 1) - expect(page).to have_selector('.gl-pagination') - expect(page).not_to have_content "You're all done!" - end - - it 'updates todo count' do - mark_all_and_undo - - expect(page).to have_content 'To do 2' - expect(page).to have_content 'Done 0' - end - - it 'shows "Mark all as done" button' do - mark_all_and_undo - - expect(page).to have_selector('.js-todos-mark-all', visible: true) - expect(page).to have_selector('.js-todos-undo-all', visible: false) - end - - context 'User has deleted a todo' do - before do - within first('.todo') do - click_link 'Done' - end - end - - it 'shows the restored todo list with the deleted todo' do - mark_all_and_undo - - expect(page).to have_selector('.todos-list .todo.todo-pending', count: 1) - end - end - - def mark_all_and_undo - find('.js-todos-mark-all').trigger('click') - wait_for_requests - find('.js-todos-undo-all').trigger('click') - wait_for_requests - end - end - end - - context 'User has a Todo in a project pending deletion' do - before do - deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true) - create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) - create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author, state: :done) - login_as(user) - visit dashboard_todos_path - end - - it 'shows "All done" message' do - within('.todos-count') { expect(page).to have_content '0' } - expect(page).to have_content 'To do 0' - expect(page).to have_content 'Done 0' - expect(page).to have_selector('.todos-all-done', count: 1) - end - end - - context 'User has a Build Failed todo' do - let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } - - before do - login_as user - visit dashboard_todos_path - end - - it 'shows the todo' do - expect(page).to have_content 'The build failed for merge request' - end - - it 'links to the pipelines for the merge request' do - href = pipelines_namespace_project_merge_request_path(project.namespace, project, todo.target) - - expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href - end - end - end -end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index c1ae6db00c6..5af2c0e9035 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -5,13 +5,15 @@ feature 'Triggers', feature: true, js: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest_user) { create(:user) } - before { login_as(user) } before do + sign_in(user) + @project = create(:empty_project) @project.team << [user, :master] @project.team << [user2, :master] @project.team << [guest_user, :guest] + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) end @@ -31,7 +33,7 @@ feature 'Triggers', feature: true, js: true do # See if "trigger creation successful" message displayed and description and owner are correct expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.' expect(page.find('.triggers-list')).to have_content 'trigger desc' - expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + expect(page.find('.triggers-list .trigger-owner')).to have_content user.name end end @@ -59,7 +61,7 @@ feature 'Triggers', feature: true, js: true do # See if "trigger updated successfully" message displayed and description and owner are correct expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.' expect(page.find('.triggers-list')).to have_content new_trigger_title - expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + expect(page.find('.triggers-list .trigger-owner')).to have_content user.name end scenario 'edit "legacy" trigger and save' do @@ -96,7 +98,7 @@ feature 'Triggers', feature: true, js: true do page.accept_confirm do expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.' expect(page.find('.triggers-list')).to have_content trigger_title - expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + expect(page.find('.triggers-list .trigger-owner')).to have_content user.name end end end @@ -155,7 +157,7 @@ feature 'Triggers', feature: true, js: true do expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard') # See if trigger owner name doesn't match with current_user and trigger is non-editable - expect(page.find('.triggers-list .trigger-owner')).not_to have_content @user.name + expect(page.find('.triggers-list .trigger-owner')).not_to have_content user.name expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') end @@ -168,7 +170,7 @@ feature 'Triggers', feature: true, js: true do expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard') # See if trigger owner name matches with current_user and is editable - expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + expect(page.find('.triggers-list .trigger-owner')).to have_content user.name expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 2fed8067042..f3662cb184f 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do - before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) } + before do + allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) + end def manage_two_factor_authentication click_on 'Manage two-factor authentication' @@ -23,12 +25,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do let(:user) { create(:user) } before do - login_as(user) + gitlab_sign_in(user) user.update_attribute(:otp_required_for_login, true) end describe 'when 2FA via OTP is disabled' do - before { user.update_attribute(:otp_required_for_login, false) } + before do + user.update_attribute(:otp_required_for_login, false) + end it 'does not allow registering a new device' do visit profile_account_path @@ -89,10 +93,10 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do manage_two_factor_authentication u2f_device = register_u2f_device expect(page).to have_content('Your U2F device was registered') - logout + gitlab_sign_out # Second user - user = login_as(:user) + user = gitlab_sign_in(:user) user.update_attribute(:otp_required_for_login, true) visit profile_account_path manage_two_factor_authentication @@ -143,18 +147,18 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do before do # Register and logout - login_as(user) + gitlab_sign_in(user) user.update_attribute(:otp_required_for_login, true) visit profile_account_path manage_two_factor_authentication @u2f_device = register_u2f_device - logout + gitlab_sign_out end describe "when 2FA via OTP is disabled" do it "allows logging in with the U2F device" do user.update_attribute(:otp_required_for_login, false) - login_with(user) + gitlab_sign_in(user) @u2f_device.respond_to_u2f_authentication @@ -166,7 +170,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do describe "when 2FA via OTP is enabled" do it "allows logging in with the U2F device" do user.update_attribute(:otp_required_for_login, true) - login_with(user) + gitlab_sign_in(user) @u2f_device.respond_to_u2f_authentication @@ -176,7 +180,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do end it 'persists remember_me value via hidden field' do - login_with(user, remember: true) + gitlab_sign_in(user, remember: true) @u2f_device.respond_to_u2f_authentication expect(page).to have_content('We heard back from your U2F device') @@ -191,15 +195,15 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do describe "but not the current user" do it "does not allow logging in with that particular device" do # Register current user with the different U2F device - current_user = login_as(:user) + current_user = gitlab_sign_in(:user) current_user.update_attribute(:otp_required_for_login, true) visit profile_account_path manage_two_factor_authentication register_u2f_device(name: 'My other device') - logout + gitlab_sign_out # Try authenticating user with the old U2F device - login_as(current_user) + gitlab_sign_in(current_user) @u2f_device.respond_to_u2f_authentication expect(page).to have_content('We heard back from your U2F device') expect(page).to have_content('Authentication via U2F device failed') @@ -209,15 +213,15 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do describe "and also the current user" do it "allows logging in with that particular device" do # Register current user with the same U2F device - current_user = login_as(:user) + current_user = gitlab_sign_in(:user) current_user.update_attribute(:otp_required_for_login, true) visit profile_account_path manage_two_factor_authentication register_u2f_device(@u2f_device) - logout + gitlab_sign_out # Try authenticating user with the same U2F device - login_as(current_user) + gitlab_sign_in(current_user) @u2f_device.respond_to_u2f_authentication expect(page).to have_content('We heard back from your U2F device') @@ -229,7 +233,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do describe "when a given U2F device has not been registered" do it "does not allow logging in with that particular device" do unregistered_device = FakeU2fDevice.new(page, 'My device') - login_as(user) + gitlab_sign_in(user) unregistered_device.respond_to_u2f_authentication expect(page).to have_content('We heard back from your U2F device') @@ -240,7 +244,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do describe "when more than one device has been registered by the same user" do it "allows logging in with either device" do # Register first device - user = login_as(:user) + user = gitlab_sign_in(:user) user.update_attribute(:otp_required_for_login, true) visit profile_two_factor_auth_path expect(page).to have_content("Your U2F device needs to be set up.") @@ -250,17 +254,17 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do visit profile_two_factor_auth_path expect(page).to have_content("Your U2F device needs to be set up.") second_device = register_u2f_device(name: 'My other device') - logout + gitlab_sign_out # Authenticate as both devices [first_device, second_device].each do |device| - login_as(user) + gitlab_sign_in(user) device.respond_to_u2f_authentication expect(page).to have_content('We heard back from your U2F device') expect(page).to have_css('.sign-out-link', visible: false) - logout + gitlab_sign_out end end end @@ -269,7 +273,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do let(:user) { create(:user) } before do - user = login_as(:user) + user = gitlab_sign_in(:user) user.update_attribute(:otp_required_for_login, true) visit profile_account_path manage_two_factor_authentication @@ -296,15 +300,15 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do before do # Register and logout - login_as(user) + gitlab_sign_in(user) user.update_attribute(:otp_required_for_login, true) visit profile_account_path end describe 'when no u2f device is registered' do before do - logout - login_with(user) + gitlab_sign_out + gitlab_sign_in(user) end it 'shows the fallback otp code UI' do @@ -316,8 +320,8 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do before do manage_two_factor_authentication @u2f_device = register_u2f_device - logout - login_with(user) + gitlab_sign_out + gitlab_sign_in(user) end it 'provides a button that shows the fallback otp code UI' do diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb index 8509551ce4a..352f8ba70ac 100644 --- a/spec/features/unsubscribe_links_spec.rb +++ b/spec/features/unsubscribe_links_spec.rb @@ -56,7 +56,9 @@ describe 'Unsubscribe links', feature: true do end context 'when logged in' do - before { login_as(recipient) } + before do + sign_in(recipient) + end it 'unsubscribes from the issue when visiting the link from the email body' do visit body_link diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb index d9d6f2e2382..797b7b3d50d 100644 --- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -5,7 +5,7 @@ feature 'User uploads avatar to group', feature: true do user = create(:user) group = create(:group) group.add_owner(user) - login_as(user) + gitlab_sign_in(user) visit edit_group_path(group) attach_file( diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb index eb8dbd76aab..a3f8027f4da 100644 --- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' feature 'User uploads avatar to profile', feature: true do scenario 'they see their new avatar' do user = create(:user) - login_as(user) + gitlab_sign_in(user) visit profile_path attach_file( diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb index 9332d3b88d2..77a1012762d 100644 --- a/spec/features/uploads/user_uploads_file_to_note_spec.rb +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -8,7 +8,7 @@ feature 'User uploads file to note', feature: true do let(:issue) { create(:issue, project: project, author: user) } before do - login_as(user) + gitlab_sign_in(user) visit namespace_project_issue_path(project.namespace, project, issue) end diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb index b84f834ff1e..7538a6e4a04 100644 --- a/spec/features/user_callout_spec.rb +++ b/spec/features/user_callout_spec.rb @@ -6,7 +6,7 @@ describe 'User Callouts', js: true do let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } before do - login_as(user) + gitlab_sign_in(user) project.team << [user, :master] end diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index c2842255b86..1bd7e038939 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -57,7 +57,7 @@ describe 'User can display performacne bar', :js do context 'when user is logged-in' do before do - login_as :user + gitlab_sign_in(create(:user)) visit root_path end diff --git a/spec/features/users/projects_spec.rb b/spec/features/users/projects_spec.rb index 67ce4b44464..377b1a0148f 100644 --- a/spec/features/users/projects_spec.rb +++ b/spec/features/users/projects_spec.rb @@ -8,7 +8,7 @@ describe 'Projects tab on a user profile', :feature, :js do before do allow(Project).to receive(:default_per_page).and_return(1) - login_as(user) + gitlab_sign_in(user) visit user_path(user) diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb index dbd5f66b55e..797b317a9bb 100644 --- a/spec/features/users/rss_spec.rb +++ b/spec/features/users/rss_spec.rb @@ -5,7 +5,7 @@ feature 'User RSS' do context 'when signed in' do before do - login_as(create(:user)) + gitlab_sign_in(create(:user)) visit path end diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb index 2e388115633..74c5cbd7887 100644 --- a/spec/features/users/snippets_spec.rb +++ b/spec/features/users/snippets_spec.rb @@ -24,7 +24,7 @@ describe 'Snippets tab on a user profile', feature: true, js: true do let!(:other_snippet) { create(:snippet, :public) } it 'contains only internal and public snippets of a user when a user is logged in' do - login_as(:user) + gitlab_sign_in(:user) visit user_path(user) page.within('.user-profile-nav') { click_link 'Snippets' } wait_for_requests diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index fbe078bd136..84af13d3e49 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -24,7 +24,7 @@ feature 'Users', feature: true, js: true do user.reload expect(user.reset_password_token).not_to be_nil - login_with(user) + gitlab_sign_in(user) expect(current_path).to eq root_path user.reload @@ -45,7 +45,9 @@ feature 'Users', feature: true, js: true do end describe 'redirect alias routes' do - before { user } + before do + expect(user).to be_persisted + end scenario '/u/user1 redirects to user page' do visit '/u/user1' diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index d0c982919db..85085bf305a 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -6,7 +6,7 @@ describe 'Project variables', js: true do let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') } before do - login_as(user) + gitlab_sign_in(user) project.team << [user, :master] project.variables << variable diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb index 5b3591550c1..9e70cccc3c4 100644 --- a/spec/finders/groups_finder_spec.rb +++ b/spec/finders/groups_finder_spec.rb @@ -38,28 +38,79 @@ describe GroupsFinder do end end - context 'subgroups' do + context 'subgroups', :nested_groups do let!(:parent_group) { create(:group, :public) } let!(:public_subgroup) { create(:group, :public, parent: parent_group) } let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) } let!(:private_subgroup) { create(:group, :private, parent: parent_group) } context 'without a user' do - it 'only returns public subgroups' do - expect(described_class.new(nil, parent: parent_group).execute).to contain_exactly(public_subgroup) + it 'only returns parent and public subgroups' do + expect(described_class.new(nil).execute).to contain_exactly(parent_group, public_subgroup) end end context 'with a user' do - it 'returns public and internal subgroups' do - expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup) + subject { described_class.new(user).execute } + + it 'returns parent, public, and internal subgroups' do + is_expected.to contain_exactly(parent_group, public_subgroup, internal_subgroup) end context 'being member' do - it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do + it 'returns parent, public subgroups, internal subgroups, and private subgroups user is member of' do private_subgroup.add_guest(user) - expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup, private_subgroup) + is_expected.to contain_exactly(parent_group, public_subgroup, internal_subgroup, private_subgroup) + end + end + + context 'parent group private' do + before do + parent_group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + context 'being member of parent group' do + it 'returns all subgroups' do + parent_group.add_guest(user) + + is_expected.to contain_exactly(parent_group, public_subgroup, internal_subgroup, private_subgroup) + end + end + + context 'authorized to private project' do + context 'project one level deep' do + let!(:subproject) { create(:empty_project, :private, namespace: private_subgroup) } + before do + subproject.add_guest(user) + end + + it 'includes the subgroup of the project' do + is_expected.to include(private_subgroup) + end + + it 'does not include private subgroups deeper down' do + subsubgroup = create(:group, :private, parent: private_subgroup) + + is_expected.not_to include(subsubgroup) + end + end + + context 'project two levels deep' do + let!(:private_subsubgroup) { create(:group, :private, parent: private_subgroup) } + let!(:subsubproject) { create(:empty_project, :private, namespace: private_subsubgroup) } + before do + subsubproject.add_guest(user) + end + + it 'returns all the ancestor groups' do + is_expected.to include(private_subsubgroup, private_subgroup, parent_group) + end + + it 'returns the groups for a given parent' do + expect(described_class.new(user, parent: parent_group).execute).to include(private_subgroup) + end + end end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 96151689359..8ace1fb5751 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -7,9 +7,9 @@ describe IssuesFinder do set(:project2) { create(:empty_project) } set(:milestone) { create(:milestone, project: project1) } set(:label) { create(:label, project: project2) } - set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab') } + set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) } set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') } - set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki') } + set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) } describe '#execute' do set(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } @@ -148,7 +148,9 @@ describe IssuesFinder do let(:params) { { label_name: [label.title, label2.title].join(',') } } let(:label2) { create(:label, project: project2) } - before { create(:label_link, label: label2, target: issue2) } + before do + create(:label_link, label: label2, target: issue2) + end it 'returns the unique issues with any of those labels' do expect(issues).to contain_exactly(issue2) @@ -213,6 +215,24 @@ describe IssuesFinder do end end + context 'filtering by created_at' do + context 'through created_after' do + let(:params) { { created_after: issue3.created_at } } + + it 'returns issues created on or after the given date' do + expect(issues).to contain_exactly(issue3) + end + end + + context 'through created_before' do + let(:params) { { created_before: issue1.created_at + 1.second } } + + it 'returns issues created on or before the given date' do + expect(issues).to contain_exactly(issue1) + end + end + end + context 'when the user is unauthorized' do let(:search_user) { nil } diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 58b7cd5e098..5eb26de6c92 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -46,5 +46,47 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request1) end + + context 'with created_after and created_before params' do + let(:project4) { create(:empty_project, forked_from_project: project1) } + + let!(:new_merge_request) do + create(:merge_request, + :simple, + author: user, + created_at: 1.week.from_now, + source_project: project4, + target_project: project1) + end + + let!(:old_merge_request) do + create(:merge_request, + :simple, + author: user, + created_at: 1.week.ago, + source_project: project4, + target_project: project4) + end + + before do + project4.add_master(user) + end + + it 'filters by created_after' do + params = { project_id: project1.id, created_after: new_merge_request.created_at } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(new_merge_request) + end + + it 'filters by created_before' do + params = { project_id: project4.id, created_before: old_merge_request.created_at + 1.second } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request) + end + end end end diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb index fd92664ca24..3f22b3a253d 100644 --- a/spec/finders/personal_access_tokens_finder_spec.rb +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -25,49 +25,65 @@ describe PersonalAccessTokensFinder do end describe 'without impersonation' do - before { params[:impersonation] = false } + before do + params[:impersonation] = false + end it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) } end end describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) } end end describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it do is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token, @@ -81,7 +97,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end @@ -93,7 +111,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end @@ -109,7 +129,9 @@ describe PersonalAccessTokensFinder do let!(:other_user_expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user2) } let!(:other_user_revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user2) } - before { params[:user] = user } + before do + params[:user] = user + end it do is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token, @@ -118,49 +140,65 @@ describe PersonalAccessTokensFinder do end describe 'without impersonation' do - before { params[:impersonation] = false } + before do + params[:impersonation] = false + end it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) } end end describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) } end end describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it do is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token, @@ -174,7 +212,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end @@ -186,7 +226,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb index e0e17af681a..304b0fb67fb 100644 --- a/spec/finders/personal_projects_finder_spec.rb +++ b/spec/finders/personal_projects_finder_spec.rb @@ -32,7 +32,9 @@ describe PersonalProjectsFinder do end context 'external' do - before { current_user.update_attributes(external: true) } + before do + current_user.update_attributes(external: true) + end it { is_expected.to eq([private_project, public_project]) } end diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb index f2aeda241c1..2b19cda35b0 100644 --- a/spec/finders/pipelines_finder_spec.rb +++ b/spec/finders/pipelines_finder_spec.rb @@ -170,7 +170,7 @@ describe PipelinesFinder do context 'when order_by and sort are specified' do context 'when order_by user_id' do let(:params) { { order_by: 'user_id', sort: 'asc' } } - let!(:pipelines) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) } + let!(:pipelines) { Array.new(2) { create(:ci_pipeline, project: project, user: create(:user)) } } it 'sorts as user_id: :asc' do is_expected.to match_array(pipelines) diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index f7e7e733cf7..8be447418b0 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -6,7 +6,9 @@ describe TodosFinder do let(:project) { create(:empty_project) } let(:finder) { described_class } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end describe '#sort' do context 'by date' do diff --git a/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json new file mode 100644 index 00000000000..47b5d283b8c --- /dev/null +++ b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json @@ -0,0 +1,58 @@ +{ + "items": { + "properties": { + "group": { + "type": "string" + }, + "metrics": { + "items": { + "properties": { + "queries": { + "items": { + "properties": { + "query_range": { + "type": "string" + }, + "query": { + "type": "string" + }, + "result": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "weight": { + "type": "integer" + }, + "y_label": { + "type": "string" + } + }, + "type": "object" + }, + "required": [ + "metrics", + "title", + "weight" + ], + "type": "array" + }, + "priority": { + "type": "integer" + } + }, + "type": "object" + }, + "required": [ + "group", + "priority", + "metrics" + ], + "type": "array" +}
\ No newline at end of file diff --git a/spec/fixtures/emails/html_empty_link.eml b/spec/fixtures/emails/html_empty_link.eml new file mode 100644 index 00000000000..1672b98b925 --- /dev/null +++ b/spec/fixtures/emails/html_empty_link.eml @@ -0,0 +1,26 @@ + +MIME-Version: 1.0 +Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT) +X-Originating-IP: [117.207.85.84] +In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +References: <topic/35@discourse.techapj.com> + <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +Date: Wed, 8 Oct 2014 10:47:17 +0530 +Delivered-To: arpit@techapj.com +Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com> +Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse! +From: Arpit Jalan <arpit@techapj.com> +To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>Accept-Language: en-US +Content-Language: en-US +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-originating-ip: [134.68.31.227] +Content-Type: multipart/alternative; + boundary="_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_" +MIME-Version: 1.0 + +--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_ +Content-Type: text/html; charset="utf-8" + +<a name="_MailEndCompose">no brackets!</a> +--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_-- diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 49df91b236f..56daeffde27 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -61,14 +61,14 @@ describe ApplicationHelper do project = create(:empty_project, avatar: File.open(uploaded_image_temp_path)) avatar_url = "/uploads/system/project/avatar/#{project.id}/banana_sample.gif" - expect(helper.project_icon(project.full_path).to_s). - to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" + expect(helper.project_icon(project.full_path).to_s) + .to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) avatar_url = "#{gitlab_host}/uploads/system/project/avatar/#{project.id}/banana_sample.gif" - expect(helper.project_icon(project.full_path).to_s). - to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" + expect(helper.project_icon(project.full_path).to_s) + .to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" end it 'gives uploaded icon when present' do @@ -82,42 +82,71 @@ describe ApplicationHelper do end describe 'avatar_icon' do - it 'returns an url for the avatar' do - user = create(:user, avatar: File.open(uploaded_image_temp_path)) - - avatar_url = "/uploads/system/user/avatar/#{user.id}/banana_sample.gif" - - expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) - - allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) - avatar_url = "#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif" - - expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) - end - - it 'returns an url for the avatar with relative url' do - stub_config_setting(relative_url_root: '/gitlab') - # Must be stubbed after the stub above, and separately - stub_config_setting(url: Settings.send(:build_gitlab_url)) - - user = create(:user, avatar: File.open(uploaded_image_temp_path)) - - expect(helper.avatar_icon(user.email).to_s). - to match("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif") - end + let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) } + + context 'using an email' do + context 'when there is a matching user' do + it 'returns a relative URL for the avatar' do + expect(helper.avatar_icon(user.email).to_s) + .to eq("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + + context 'when an asset_host is set in the config' do + let(:asset_host) { 'http://assets' } + + before do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + end + + it 'returns an absolute URL on that asset host' do + expect(helper.avatar_icon(user.email, only_path: false).to_s) + .to eq("#{asset_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end + + context 'when only_path is set to false' do + it 'returns an absolute URL for the avatar' do + expect(helper.avatar_icon(user.email, only_path: false).to_s) + .to eq("#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end + + context 'when the GitLab instance is at a relative URL' do + before do + stub_config_setting(relative_url_root: '/gitlab') + # Must be stubbed after the stub above, and separately + stub_config_setting(url: Settings.send(:build_gitlab_url)) + end + + it 'returns a relative URL with the correct prefix' do + expect(helper.avatar_icon(user.email).to_s) + .to eq("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end + end - it 'calls gravatar_icon when no User exists with the given email' do - expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) + context 'when no user exists for the email' do + it 'calls gravatar_icon' do + expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) - helper.avatar_icon('foo@example.com', 20, 2) + helper.avatar_icon('foo@example.com', 20, 2) + end + end end - describe 'using a User' do - it 'returns an URL for the avatar' do - user = create(:user, avatar: File.open(uploaded_image_temp_path)) + describe 'using a user' do + context 'when only_path is true' do + it 'returns a relative URL for the avatar' do + expect(helper.avatar_icon(user, only_path: true).to_s) + .to eq("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end - expect(helper.avatar_icon(user).to_s). - to match("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + context 'when only_path is false' do + it 'returns an absolute URL for the avatar' do + expect(helper.avatar_icon(user, only_path: false).to_s) + .to eq("#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end end end end @@ -147,22 +176,22 @@ describe ApplicationHelper do it 'returns a valid Gravatar URL' do stub_config_setting(https: false) - expect(helper.gravatar_icon(user_email)). - to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') + expect(helper.gravatar_icon(user_email)) + .to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') end it 'uses HTTPs when configured' do stub_config_setting(https: true) - expect(helper.gravatar_icon(user_email)). - to match('https://secure.gravatar.com') + expect(helper.gravatar_icon(user_email)) + .to match('https://secure.gravatar.com') end it 'returns custom gravatar path when gravatar_url is set' do stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}') - expect(gravatar_icon(user_email, 20)). - to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118') + expect(gravatar_icon(user_email, 20)) + .to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118') end it 'accepts a custom size argument' do @@ -234,8 +263,8 @@ describe ApplicationHelper do end it 'accepts a custom html_class' do - expect(element(html_class: 'custom_class').attr('class')). - to eq 'js-timeago custom_class' + expect(element(html_class: 'custom_class').attr('class')) + .to eq 'js-timeago custom_class' end it 'accepts a custom tooltip placement' do @@ -257,4 +286,24 @@ describe ApplicationHelper do it { expect(helper.active_when(true)).to eq('active') } it { expect(helper.active_when(false)).to eq(nil) } end + + describe '#support_url' do + context 'when alternate support url is specified' do + let(:alternate_url) { 'http://company.example.com/getting-help' } + + before do + allow(current_application_settings).to receive(:help_page_support_url) { alternate_url } + end + + it 'returns the alternate support url' do + expect(helper.support_url).to eq(alternate_url) + end + end + + context 'when alternate support url is not specified' do + it 'builds the support url from the promo_url' do + expect(helper.support_url).to eq(helper.promo_url + '/getting-help/') + end + end + end end diff --git a/spec/helpers/blame_helper_spec.rb b/spec/helpers/blame_helper_spec.rb new file mode 100644 index 00000000000..b4368516d83 --- /dev/null +++ b/spec/helpers/blame_helper_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe BlameHelper do + describe '#get_age_map_start_date' do + let(:dates) do + [Time.zone.local(2014, 3, 17, 0, 0, 0), + Time.zone.local(2011, 11, 2, 0, 0, 0), + Time.zone.local(2015, 7, 9, 0, 0, 0), + Time.zone.local(2013, 2, 24, 0, 0, 0), + Time.zone.local(2010, 9, 22, 0, 0, 0)] + end + let(:blame_groups) do + [ + { commit: double(committed_date: dates[0]) }, + { commit: double(committed_date: dates[1]) }, + { commit: double(committed_date: dates[2]) } + ] + end + + it 'returns the earliest date from a blame group' do + project = double(created_at: dates[3]) + + duration = helper.age_map_duration(blame_groups, project) + + expect(duration[:started_days_ago]).to eq((duration[:now] - dates[1]).to_i / 1.day) + end + + it 'returns the earliest date from a project' do + project = double(created_at: dates[4]) + + duration = helper.age_map_duration(blame_groups, project) + + expect(duration[:started_days_ago]).to eq((duration[:now] - dates[4]).to_i / 1.day) + end + end + + describe '#age_map_class' do + let(:dates) do + [Time.zone.local(2014, 3, 17, 0, 0, 0)] + end + let(:blame_groups) do + [ + { commit: double(committed_date: dates[0]) } + ] + end + let(:duration) do + project = double(created_at: dates[0]) + helper.age_map_duration(blame_groups, project) + end + + it 'returns blame-commit-age-9 when oldest' do + expect(helper.age_map_class(dates[0], duration)).to eq 'blame-commit-age-9' + end + + it 'returns blame-commit-age-0 class when newest' do + expect(helper.age_map_class(duration[:now], duration)).to eq 'blame-commit-age-0' + end + end +end diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb index c6e3c5c2368..9bec0f9f432 100644 --- a/spec/helpers/broadcast_messages_helper_spec.rb +++ b/spec/helpers/broadcast_messages_helper_spec.rb @@ -33,8 +33,8 @@ describe BroadcastMessagesHelper do it 'allows custom style' do broadcast_message = double(color: '#f2dede', font: '#b94a48') - expect(helper.broadcast_message_style(broadcast_message)). - to match('background-color: #f2dede; color: #b94a48') + expect(helper.broadcast_message_style(broadcast_message)) + .to match('background-color: #f2dede; color: #b94a48') end end diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb new file mode 100644 index 00000000000..661327d4432 --- /dev/null +++ b/spec/helpers/button_helper_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe ButtonHelper do + describe 'http_clone_button' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:has_tooltip_class) { 'has-tooltip' } + + def element + element = helper.http_clone_button(project) + + Nokogiri::HTML::DocumentFragment.parse(element).first_element_child + end + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'with internal auth enabled' do + context 'when user has a password' do + it 'shows no tooltip' do + expect(element.attr('class')).not_to include(has_tooltip_class) + end + end + + context 'when user has password automatically set' do + let(:user) { create(:user, password_automatically_set: true) } + + it 'shows a password tooltip' do + expect(element.attr('class')).to include(has_tooltip_class) + expect(element.attr('data-title')).to eq('Set a password on your account to pull or push via HTTP.') + end + end + end + + context 'with internal auth disabled' do + before do + stub_application_setting(signin_enabled?: false) + end + + context 'when user has no personal access tokens' do + it 'has a personal access token tooltip ' do + expect(element.attr('class')).to include(has_tooltip_class) + expect(element.attr('data-title')).to eq('Create a personal access token on your account to pull or push via HTTP.') + end + end + + context 'when user has a personal access token' do + it 'shows no tooltip' do + create(:personal_access_token, user: user) + + expect(element.attr('class')).not_to include(has_tooltip_class) + end + end + end + + context 'when user is ldap user' do + let(:user) { create(:omniauth_user, password_automatically_set: true) } + + it 'shows no tooltip' do + expect(element.attr('class')).not_to include(has_tooltip_class) + end + end + end +end diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index a2c008790f9..c245bb439db 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -9,8 +9,8 @@ describe CommitsHelper do author_email: 'my@email.com" onmouseover="alert(1)' ) - expect(helper.commit_author_link(commit)). - not_to include('onmouseover="alert(1)"') + expect(helper.commit_author_link(commit)) + .not_to include('onmouseover="alert(1)"') end end @@ -22,8 +22,8 @@ describe CommitsHelper do committer_email: 'my@email.com" onmouseover="alert(1)' ) - expect(helper.commit_committer_link(commit)). - not_to include('onmouseover="alert(1)"') + expect(helper.commit_committer_link(commit)) + .not_to include('onmouseover="alert(1)"') end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index a74615e07f9..0d909e6e140 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -8,7 +8,7 @@ describe DiffHelper do let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.raw_diffs } let(:diff) { diffs.first } - let(:diff_refs) { [commit.parent, commit] } + let(:diff_refs) { commit.diff_refs } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } describe 'diff_view' do @@ -148,12 +148,21 @@ describe DiffHelper do it 'puts comments on added lines' do left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3) - right = Gitlab::Diff::Line.new('new line', 'add', 3, 3, 3) + right = Gitlab::Diff::Line.new('new line', 'new', 3, 3, 3) result = helper.parallel_diff_discussions(left, right, diff_file) expect(result).to eq([nil, 'comment']) end + + it 'puts comments on unchanged lines' do + left = Gitlab::Diff::Line.new('unchanged line', nil, 3, 3, 3) + right = Gitlab::Diff::Line.new('unchanged line', nil, 3, 3, 3) + + result = helper.parallel_diff_discussions(left, right, diff_file) + + expect(result).to eq(['comment', nil]) + end end describe "#diff_match_line" do @@ -207,4 +216,41 @@ describe DiffHelper do expect(output).not_to have_css 'td:nth-child(3)' end end + + context 'viewer related' do + let(:viewer) { diff_file.simple_viewer } + + before do + assign(:project, project) + end + + describe '#diff_render_error_reason' do + context 'for error :too_large' do + before do + expect(viewer).to receive(:render_error).and_return(:too_large) + end + + it 'returns an error message' do + expect(helper.diff_render_error_reason(viewer)).to eq('it is too large') + end + end + + context 'for error :server_side_but_stored_externally' do + before do + expect(viewer).to receive(:render_error).and_return(:server_side_but_stored_externally) + expect(diff_file).to receive(:external_storage).and_return(:lfs) + end + + it 'returns an error message' do + expect(helper.diff_render_error_reason(viewer)).to eq('it is stored in LFS') + end + end + end + + describe '#diff_render_error_options' do + it 'includes a "view the blob" link' do + expect(helper.diff_render_error_options(viewer)).to include(/view the blob/) + end + end + end end diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb index b20373a96fb..18cf0031d5f 100644 --- a/spec/helpers/form_helper_spec.rb +++ b/spec/helpers/form_helper_spec.rb @@ -11,18 +11,18 @@ describe FormHelper do it 'renders an alert div' do model = double(errors: errors_stub('Error 1')) - expect(helper.form_errors(model)). - to include('<div class="alert alert-danger" id="error_explanation">') + expect(helper.form_errors(model)) + .to include('<div class="alert alert-danger" id="error_explanation">') end it 'contains a summary message' do single_error = double(errors: errors_stub('A')) multi_errors = double(errors: errors_stub('A', 'B', 'C')) - expect(helper.form_errors(single_error)). - to include('<h4>The form contains the following error:') - expect(helper.form_errors(multi_errors)). - to include('<h4>The form contains the following errors:') + expect(helper.form_errors(single_error)) + .to include('<h4>The form contains the following error:') + expect(helper.form_errors(multi_errors)) + .to include('<h4>The form contains the following errors:') end it 'renders each message' do diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 0337afa4452..8da22dc78fa 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe GroupsHelper do + include ApplicationHelper + describe 'group_icon' do avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') @@ -8,8 +10,8 @@ describe GroupsHelper do group = create(:group) group.avatar = fixture_file_upload(avatar_file_path) group.save! - expect(group_icon(group.path).to_s). - to match("/uploads/system/group/avatar/#{group.id}/banana_sample.gif") + expect(group_icon(group.path).to_s) + .to match("/uploads/system/group/avatar/#{group.id}/banana_sample.gif") end it 'gives default avatar_icon when no avatar is present' do @@ -81,4 +83,15 @@ describe GroupsHelper do end end end + + describe 'group_title', :nested_groups do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'outputs the groups in the correct order' do + expect(group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) + end + end end diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index 10f293cddf5..9afff47f4e9 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -29,21 +29,21 @@ describe ImportHelper do context 'when provider is "github"' do context 'when provider does not specify a custom URL' do it 'uses default GitHub URL' do - allow(Gitlab.config.omniauth).to receive(:providers). - and_return([Settingslogic.new('name' => 'github')]) + allow(Gitlab.config.omniauth).to receive(:providers) + .and_return([Settingslogic.new('name' => 'github')]) - expect(helper.provider_project_link('github', 'octocat/Hello-World')). - to include('href="https://github.com/octocat/Hello-World"') + expect(helper.provider_project_link('github', 'octocat/Hello-World')) + .to include('href="https://github.com/octocat/Hello-World"') end end context 'when provider specify a custom URL' do it 'uses custom URL' do - allow(Gitlab.config.omniauth).to receive(:providers). - and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')]) + allow(Gitlab.config.omniauth).to receive(:providers) + .and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')]) - expect(helper.provider_project_link('github', 'octocat/Hello-World')). - to include('href="https://github.company.com/octocat/Hello-World"') + expect(helper.provider_project_link('github', 'octocat/Hello-World')) + .to include('href="https://github.company.com/octocat/Hello-World"') end end end @@ -54,8 +54,8 @@ describe ImportHelper do end it 'uses given host' do - expect(helper.provider_project_link('gitea', 'octocat/Hello-World')). - to include('href="https://try.gitea.io/octocat/Hello-World"') + expect(helper.provider_project_link('gitea', 'octocat/Hello-World')) + .to include('href="https://try.gitea.io/octocat/Hello-World"') end end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 8fcf7f5fa15..15cb620199d 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -40,23 +40,23 @@ describe IssuablesHelper do end it 'returns "Open" when state is :opened' do - expect(helper.issuables_state_counter_text(:issues, :opened)). - to eq('<span>Open</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to eq('<span>Open</span> <span class="badge">42</span>') end it 'returns "Closed" when state is :closed' do - expect(helper.issuables_state_counter_text(:issues, :closed)). - to eq('<span>Closed</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :closed)) + .to eq('<span>Closed</span> <span class="badge">42</span>') end it 'returns "Merged" when state is :merged' do - expect(helper.issuables_state_counter_text(:merge_requests, :merged)). - to eq('<span>Merged</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:merge_requests, :merged)) + .to eq('<span>Merged</span> <span class="badge">42</span>') end it 'returns "All" when state is :all' do - expect(helper.issuables_state_counter_text(:merge_requests, :all)). - to eq('<span>All</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:merge_requests, :all)) + .to eq('<span>All</span> <span class="badge">42</span>') end end @@ -81,13 +81,13 @@ describe IssuablesHelper do expect(helper).to receive(:params).twice.and_return(params) expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) - expect(helper.issuables_state_counter_text(:issues, :opened)). - to eq('<span>Open</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to eq('<span>Open</span> <span class="badge">42</span>') expect(helper).not_to receive(:issuables_count_for_state) - expect(helper.issuables_state_counter_text(:issues, :opened)). - to eq('<span>Open</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to eq('<span>Open</span> <span class="badge">42</span>') end it 'does not take some keys into account in the cache key' do @@ -100,8 +100,8 @@ describe IssuablesHelper do }.with_indifferent_access) expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) - expect(helper.issuables_state_counter_text(:issues, :opened)). - to eq('<span>Open</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to eq('<span>Open</span> <span class="badge">42</span>') expect(helper).to receive(:params).and_return({ author_id: '11', @@ -112,22 +112,22 @@ describe IssuablesHelper do }.with_indifferent_access) expect(helper).not_to receive(:issuables_count_for_state) - expect(helper.issuables_state_counter_text(:issues, :opened)). - to eq('<span>Open</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to eq('<span>Open</span> <span class="badge">42</span>') end it 'does not take params order into account in the cache key' do expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) - expect(helper.issuables_state_counter_text(:issues, :opened)). - to eq('<span>Open</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to eq('<span>Open</span> <span class="badge">42</span>') expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') expect(helper).not_to receive(:issuables_count_for_state) - expect(helper.issuables_state_counter_text(:issues, :opened)). - to eq('<span>Open</span> <span class="badge">42</span>') + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to eq('<span>Open</span> <span class="badge">42</span>') end end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 540cb0ab1e0..00db98fd9d2 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -93,8 +93,8 @@ describe IssuesHelper do award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane')) awards = Array.new(5, award).push(my_award) - expect(award_user_list(awards, current_user, limit: 2)). - to eq("You, Jane, and 4 more.") + expect(award_user_list(awards, current_user, limit: 2)) + .to eq("You, Jane, and 4 more.") end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 7cf535fadae..a8d6044fda7 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -55,8 +55,8 @@ describe LabelsHelper do context 'without block' do it 'uses render_colored_label as the link content' do - expect(self).to receive(:render_colored_label). - with(label, tooltip: true).and_return('Foo') + expect(self).to receive(:render_colored_label) + .with(label, tooltip: true).and_return('Foo') expect(link_to_label(label)).to match('Foo') end end diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index 2a0de0b0656..b4226f96a04 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -68,8 +68,8 @@ describe MarkupHelper do expect(doc.css('a')[0].text).to eq 'This should finally fix ' # First issue link - expect(doc.css('a')[1].attr('href')). - to eq namespace_project_issue_path(project.namespace, project, issues[0]) + expect(doc.css('a')[1].attr('href')) + .to eq namespace_project_issue_path(project.namespace, project, issues[0]) expect(doc.css('a')[1].text).to eq issues[0].to_reference # Internal commit link @@ -77,8 +77,8 @@ describe MarkupHelper do expect(doc.css('a')[2].text).to eq ' and ' # Second issue link - expect(doc.css('a')[3].attr('href')). - to eq namespace_project_issue_path(project.namespace, project, issues[1]) + expect(doc.css('a')[3].attr('href')) + .to eq namespace_project_issue_path(project.namespace, project, issues[1]) expect(doc.css('a')[3].text).to eq issues[1].to_reference # Trailing commit link @@ -98,8 +98,8 @@ describe MarkupHelper do it "escapes HTML passed in as the body" do actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}" - expect(helper.link_to_gfm(actual, link)). - to match('<h1>test</h1>') + expect(helper.link_to_gfm(actual, link)) + .to match('<h1>test</h1>') end it 'ignores reference links when they are the entire body' do @@ -110,8 +110,8 @@ describe MarkupHelper do it 'replaces commit message with emoji to link' do actual = link_to_gfm(':book: Book', '/foo') - expect(actual). - to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>' + expect(actual) + .to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>' end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index f2c9d927388..493a4ff9a93 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -15,8 +15,8 @@ describe MergeRequestsHelper do end it 'does not include api credentials in a link' do - allow(ci_service). - to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c") + allow(ci_service) + .to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c") expect(helper.ci_build_details_path(merge_request)).not_to match("secret") end end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index dff2784f21f..95b4032616e 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -86,8 +86,8 @@ describe PageLayoutHelper do it 'raises ArgumentError when given more than two attributes' do map = { foo: 'foo', bar: 'bar', baz: 'baz' } - expect { helper.page_card_attributes(map) }. - to raise_error(ArgumentError, /more than two attributes/) + expect { helper.page_card_attributes(map) } + .to raise_error(ArgumentError, /more than two attributes/) end it 'rejects blank values' do diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 2c0e9975f73..a04c87b08eb 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -29,15 +29,15 @@ describe PreferencesHelper do describe 'user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do - allow(helper).to receive(:current_user). - and_return(double(color_scheme_id: 3)) + allow(helper).to receive(:current_user) + .and_return(double(color_scheme_id: 3)) expect(helper.user_color_scheme).to eq 'solarized-light' end it 'returns the default when id is invalid' do - allow(helper).to receive(:current_user). - and_return(double(color_scheme_id: Gitlab::ColorSchemes.count + 5)) + allow(helper).to receive(:current_user) + .and_return(double(color_scheme_id: Gitlab::ColorSchemes.count + 5)) end end @@ -45,8 +45,8 @@ describe PreferencesHelper do it 'returns the default theme' do stub_user - expect(helper.user_color_scheme). - to eq Gitlab::ColorSchemes.default.css_class + expect(helper.user_color_scheme) + .to eq Gitlab::ColorSchemes.default.css_class end end end @@ -55,8 +55,8 @@ describe PreferencesHelper do if messages.empty? allow(helper).to receive(:current_user).and_return(nil) else - allow(helper).to receive(:current_user). - and_return(double('user', messages)) + allow(helper).to receive(:current_user) + .and_return(double('user', messages)) end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index a695621b87a..487d9800707 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -115,6 +115,82 @@ describe ProjectsHelper do end end + describe '#show_no_ssh_key_message?' do + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'user has no keys' do + it 'returns true' do + expect(helper.show_no_ssh_key_message?).to be_truthy + end + end + + context 'user has an ssh key' do + it 'returns false' do + create(:personal_key, user: user) + + expect(helper.show_no_ssh_key_message?).to be_falsey + end + end + end + + describe '#show_no_password_message?' do + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'user has password set' do + it 'returns false' do + expect(helper.show_no_password_message?).to be_falsey + end + end + + context 'user requires a password' do + let(:user) { create(:user, password_automatically_set: true) } + + it 'returns true' do + expect(helper.show_no_password_message?).to be_truthy + end + end + + context 'user requires a personal access token' do + it 'returns true' do + stub_application_setting(signin_enabled?: false) + + expect(helper.show_no_password_message?).to be_truthy + end + end + end + + describe '#link_to_set_password' do + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'user requires a password' do + let(:user) { create(:user, password_automatically_set: true) } + + it 'returns link to set a password' do + expect(helper.link_to_set_password).to match %r{<a href="#{edit_profile_password_path}">set a password</a>} + end + end + + context 'user requires a personal access token' do + let(:user) { create(:user) } + + it 'returns link to create a personal access token' do + stub_application_setting(signin_enabled?: false) + + expect(helper.link_to_set_password).to match %r{<a href="#{profile_personal_access_tokens_path}">create a personal access token</a>} + end + end + end + describe 'link_to_member' do let(:group) { create(:group) } let(:project) { create(:empty_project, group: group) } @@ -250,7 +326,9 @@ describe ProjectsHelper do end context "when project is private" do - before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) } + before do + project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end it "shows only allowed options" do helper.instance_variable_set(:@project, project) @@ -300,4 +378,37 @@ describe ProjectsHelper do expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Private') end end + + describe '#get_project_nav_tabs' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + allow(helper).to receive(:can?) { true } + end + + subject do + helper.send(:get_project_nav_tabs, project, user) + end + + context 'when builds feature is enabled' do + before do + allow(project).to receive(:builds_enabled?).and_return(true) + end + + it "does include pipelines tab" do + is_expected.to include(:pipelines) + end + end + + context 'when builds feature is disabled' do + before do + allow(project).to receive(:builds_enabled?).and_return(false) + end + + it "do not include pipelines tab" do + is_expected.not_to include(:pipelines) + end + end + end end diff --git a/spec/initializers/8_metrics_spec.rb b/spec/initializers/8_metrics_spec.rb index 570754621f3..a507d7f7f2b 100644 --- a/spec/initializers/8_metrics_spec.rb +++ b/spec/initializers/8_metrics_spec.rb @@ -7,6 +7,7 @@ describe 'instrument_classes', lib: true do before do allow(config).to receive(:instrument_method) allow(config).to receive(:instrument_methods) + allow(config).to receive(:instrument_instance_method) allow(config).to receive(:instrument_instance_methods) end diff --git a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js index 1ed96a67478..ec2c549e032 100644 --- a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js +++ b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js @@ -1,4 +1,4 @@ -import { getUnicodeSupportMap } from '~/behaviors/gl_emoji/unicode_support_map'; +import getUnicodeSupportMap from '~/emoji/support/unicode_support_map'; import AccessorUtilities from '~/lib/utils/accessor'; describe('Unicode Support Map', () => { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index f56b99f8a16..6dc48f9a293 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -40,16 +40,29 @@ import '~/behaviors/quick_submit'; it('disables input of type submit', function() { const submitButton = $('.js-quick-submit input[type=submit]'); this.textarea.trigger(keydownEvent()); + expect(submitButton).toBeDisabled(); }); it('disables button of type submit', function() { - // button doesn't exist in fixture, add it manually - const submitButton = $('<button type="submit">Submit it</button>'); - submitButton.insertAfter(this.textarea); - + const submitButton = $('.js-quick-submit input[type=submit]'); this.textarea.trigger(keydownEvent()); + expect(submitButton).toBeDisabled(); }); + it('only clicks one submit', function() { + const existingSubmit = $('.js-quick-submit input[type=submit]'); + // Add an extra submit button + const newSubmit = $('<button type="submit">Submit it</button>'); + newSubmit.insertAfter(this.textarea); + + const oldClick = spyOnEvent(existingSubmit, 'click'); + const newClick = spyOnEvent(newSubmit, 'click'); + + this.textarea.trigger(keydownEvent()); + + expect(oldClick).not.toHaveBeenTriggered(); + expect(newClick).toHaveBeenTriggered(); + }); // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll // only run the tests that apply to the current platform if (navigator.userAgent.match(/Macintosh/)) { diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 832877de71c..c0a7323a505 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -12,6 +12,7 @@ import './mock_data'; describe('Issue boards new issue form', () => { let vm; let list; + let newIssueMock; const promiseReturn = { json() { return { @@ -21,7 +22,11 @@ describe('Issue boards new issue form', () => { }; const submitIssue = () => { - vm.$el.querySelector('.btn-success').click(); + const dummySubmitEvent = { + preventDefault() {}, + }; + vm.$refs.submitButton = vm.$el.querySelector('.btn-success'); + return vm.submit(dummySubmitEvent); }; beforeEach((done) => { @@ -32,29 +37,35 @@ describe('Issue boards new issue form', () => { gl.issueBoards.BoardsStore.create(); gl.IssueBoardsApp = new Vue(); - setTimeout(() => { - list = new List(listObj); - - spyOn(gl.boardService, 'newIssue').and.callFake(() => new Promise((resolve, reject) => { - if (vm.title === 'error') { - reject(); - } else { - resolve(promiseReturn); - } - })); - - vm = new BoardNewIssueComp({ - propsData: { - list, - }, - }).$mount(); - - done(); - }, 0); + list = new List(listObj); + + newIssueMock = Promise.resolve(promiseReturn); + spyOn(list, 'newIssue').and.callFake(() => newIssueMock); + + vm = new BoardNewIssueComp({ + propsData: { + list, + }, + }).$mount(); + + Vue.nextTick() + .then(done) + .catch(done.fail); }); - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + it('calls submit if submit button is clicked', (done) => { + spyOn(vm, 'submit'); + vm.title = 'Testing Title'; + + Vue.nextTick() + .then(() => { + vm.$el.querySelector('.btn-success').click(); + + expect(vm.submit.calls.count()).toBe(1); + expect(vm.$refs['submit-button']).toBe(vm.$el.querySelector('.btn-success')); + }) + .then(done) + .catch(done.fail); }); it('disables submit button if title is empty', () => { @@ -64,136 +75,122 @@ describe('Issue boards new issue form', () => { it('enables submit button if title is not empty', (done) => { vm.title = 'Testing Title'; - setTimeout(() => { - expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title'); - expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true); - - done(); - }, 0); + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title'); + expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true); + }) + .then(done) + .catch(done.fail); }); it('clears title after clicking cancel', (done) => { vm.$el.querySelector('.btn-default').click(); - setTimeout(() => { - expect(vm.title).toBe(''); - done(); - }, 0); + Vue.nextTick() + .then(() => { + expect(vm.title).toBe(''); + }) + .then(done) + .catch(done.fail); }); it('does not create new issue if title is empty', (done) => { - submitIssue(); - - setTimeout(() => { - expect(gl.boardService.newIssue).not.toHaveBeenCalled(); - done(); - }, 0); + submitIssue() + .then(() => { + expect(list.newIssue).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); describe('submit success', () => { it('creates new issue', (done) => { vm.title = 'submit title'; - setTimeout(() => { - submitIssue(); - - expect(gl.boardService.newIssue).toHaveBeenCalled(); - done(); - }, 0); + Vue.nextTick() + .then(submitIssue) + .then(() => { + expect(list.newIssue).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); it('enables button after submit', (done) => { vm.title = 'submit issue'; - setTimeout(() => { - submitIssue(); - - expect(vm.$el.querySelector('.btn-success').disabled).toBe(false); - done(); - }, 0); + Vue.nextTick() + .then(submitIssue) + .then(() => { + expect(vm.$el.querySelector('.btn-success').disabled).toBe(false); + }) + .then(done) + .catch(done.fail); }); it('clears title after submit', (done) => { vm.title = 'submit issue'; - Vue.nextTick(() => { - submitIssue(); - - setTimeout(() => { + Vue.nextTick() + .then(submitIssue) + .then(() => { expect(vm.title).toBe(''); - done(); - }, 0); - }); - }); - - it('adds new issue to top of list after submit request', (done) => { - vm.title = 'submit issue'; - - setTimeout(() => { - submitIssue(); - - setTimeout(() => { - expect(list.issues.length).toBe(2); - expect(list.issues[0].title).toBe('submit issue'); - expect(list.issues[0].subscribed).toBe(true); - done(); - }, 0); - }, 0); + }) + .then(done) + .catch(done.fail); }); it('sets detail issue after submit', (done) => { expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe(undefined); vm.title = 'submit issue'; - setTimeout(() => { - submitIssue(); - - setTimeout(() => { + Vue.nextTick() + .then(submitIssue) + .then(() => { expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); - done(); - }, 0); - }, 0); + }) + .then(done) + .catch(done.fail); }); it('sets detail list after submit', (done) => { vm.title = 'submit issue'; - setTimeout(() => { - submitIssue(); - - setTimeout(() => { + Vue.nextTick() + .then(submitIssue) + .then(() => { expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); - done(); - }, 0); - }, 0); + }) + .then(done) + .catch(done.fail); }); }); describe('submit error', () => { - it('removes issue', (done) => { + beforeEach(() => { + newIssueMock = Promise.reject(new Error('My hovercraft is full of eels!')); vm.title = 'error'; + }); - setTimeout(() => { - submitIssue(); - - setTimeout(() => { + it('removes issue', (done) => { + Vue.nextTick() + .then(submitIssue) + .then(() => { expect(list.issues.length).toBe(1); - done(); - }, 0); - }, 0); + }) + .then(done) + .catch(done.fail); }); it('shows error', (done) => { - vm.title = 'error'; - - setTimeout(() => { - submitIssue(); - - setTimeout(() => { + Vue.nextTick() + .then(submitIssue) + .then(() => { expect(vm.error).toBe(true); - done(); - }, 0); - }, 0); + }) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 8e3d9fd77a0..db50829a276 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -150,4 +150,41 @@ describe('List model', () => { expect(list.getIssues).toHaveBeenCalled(); }); }); + + describe('newIssue', () => { + beforeEach(() => { + spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({ + json() { + return { + iid: 42, + }; + }, + })); + }); + + it('adds new issue to top of list', (done) => { + list.issues.push(new ListIssue({ + title: 'Testing', + iid: _.random(10000), + confidential: false, + labels: [list.label], + assignees: [], + })); + const dummyIssue = new ListIssue({ + title: 'new issue', + iid: _.random(10000), + confidential: false, + labels: [list.label], + assignees: [], + }); + + list.newIssue(dummyIssue) + .then(() => { + expect(list.issues.length).toBe(2); + expect(list.issues[0]).toBe(dummyIssue); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js index a27dc48b3fd..93dc60d59fe 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js @@ -1,15 +1,6 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; (() => { - // TODO: remove this hack! - // PhantomJS causes spyOn to panic because replaceState isn't "writable" - let phantomjs; - try { - phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; - } catch (err) { - phantomjs = false; - } - describe('Linked Tabs', () => { preloadFixtures('static/linked_tabs.html.raw'); @@ -19,9 +10,7 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; describe('when is initialized', () => { beforeEach(() => { - if (!phantomjs) { - spyOn(window.history, 'replaceState').and.callFake(function () {}); - } + spyOn(window.history, 'replaceState').and.callFake(function () {}); }); it('should activate the tab correspondent to the given action', () => { @@ -47,7 +36,7 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; describe('on click', () => { it('should change the url according to the clicked tab', () => { - const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {}); + const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); const linkedTabs = new LinkedTabs({ action: 'show', diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index ebfd60198b2..694f94efcff 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,15 +1,15 @@ import Vue from 'vue'; -import PipelinesTable from '~/commit/pipelines/pipelines_table'; +import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; describe('Pipelines table in Commits and Merge requests', () => { const jsonFixtureName = 'pipelines/pipelines.json'; let pipeline; + let PipelinesTable; - preloadFixtures('static/pipelines_table.html.raw'); preloadFixtures(jsonFixtureName); beforeEach(() => { - loadFixtures('static/pipelines_table.html.raw'); + PipelinesTable = Vue.extend(pipelinesTable); const pipelines = getJSONFixture(jsonFixtureName).pipelines; pipeline = pipelines.find(p => p.id === 1); }); @@ -26,8 +26,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesEmptyResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(function () { @@ -58,8 +61,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(() => { @@ -92,8 +98,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesErrorResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(function () { diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 44a4386b250..ace95000468 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -5,15 +5,6 @@ import '~/pager'; import '~/commits'; (() => { - // TODO: remove this hack! - // PhantomJS causes spyOn to panic because replaceState isn't "writable" - let phantomjs; - try { - phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; - } catch (err) { - phantomjs = false; - } - describe('Commits List', () => { beforeEach(() => { setFixtures(` @@ -61,9 +52,7 @@ import '~/commits'; CommitsList.init(25); CommitsList.searchField.val(''); - if (!phantomjs) { - spyOn(history, 'replaceState').and.stub(); - } + spyOn(history, 'replaceState').and.stub(); ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { req.success({ data: '<li>Result</li>', diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index e54ea11b08c..3391cade541 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -16,6 +16,10 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; const date = new Date(); date.setFullYear(date.getFullYear() + 1); + // Add a day to prevent a transient error. If date is even 1 second + // short of a full year, timeFor will return '11 months remaining' + date.setDate(date.getDate() + 1); + expect( gl.utils.timeFor(date), ).toBe('1 year remaining'); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index a4b98f6140d..5b64cbb2dfc 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -14,6 +14,7 @@ describe('Deploy keys key', () => { propsData: { deployKey, store, + endpoint: 'https://test.host/dummy/endpoint', }, }).$mount(); }; diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js index a69b39c35c4..08357d2b547 100644 --- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js +++ b/spec/javascripts/deploy_keys/components/keys_panel_spec.js @@ -17,6 +17,7 @@ describe('Deploy keys panel', () => { keys: data.enabled_keys, showHelpBox: true, store, + endpoint: 'https://test.host/dummy/endpoint', }, }).$mount(); diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/emoji_spec.js index a09e0072fa8..fa11c602ec3 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/emoji_spec.js @@ -1,12 +1,11 @@ -import { glEmojiTag } from '~/behaviors/gl_emoji'; -import { - isEmojiUnicodeSupported, +import { glEmojiTag } from '~/emoji'; +import isEmojiUnicodeSupported, { isFlagEmoji, isKeycapEmoji, isSkinToneComboEmoji, isHorceRacingSkinToneComboEmoji, isPersonZwjEmoji, -} from '~/behaviors/gl_emoji/is_emoji_unicode_supported'; +} from '~/emoji/support/is_emoji_unicode_supported'; const emptySupportMap = { personZwj: false, diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 596d812c724..ea40a1fcd4b 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -32,9 +32,16 @@ describe('Actions Component', () => { }).$mount(); }); + describe('computed', () => { + it('title', () => { + expect(component.title).toEqual('Deploy to...'); + }); + }); + it('should render a dropdown button with icon and title attribute', () => { expect(component.$el.querySelector('.fa-caret-down')).toBeDefined(); - expect(component.$el.querySelector('.dropdown-new').getAttribute('title')).toEqual('Deploy to...'); + expect(component.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual('Deploy to...'); + expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual('Deploy to...'); }); it('should render a dropdown with the provided list of actions', () => { diff --git a/spec/javascripts/environments/environment_monitoring_spec.js b/spec/javascripts/environments/environment_monitoring_spec.js index 0f3dba66230..f8d8223967a 100644 --- a/spec/javascripts/environments/environment_monitoring_spec.js +++ b/spec/javascripts/environments/environment_monitoring_spec.js @@ -3,21 +3,30 @@ import monitoringComp from '~/environments/components/environment_monitoring.vue describe('Monitoring Component', () => { let MonitoringComponent; + let component; + + const monitoringUrl = 'https://gitlab.com'; beforeEach(() => { MonitoringComponent = Vue.extend(monitoringComp); - }); - it('should render a link to environment monitoring page', () => { - const monitoringUrl = 'https://gitlab.com'; - const component = new MonitoringComponent({ + component = new MonitoringComponent({ propsData: { monitoringUrl, }, }).$mount(); + }); + describe('computed', () => { + it('title', () => { + expect(component.title).toEqual('Monitoring'); + }); + }); + + it('should render a link to environment monitoring page', () => { expect(component.$el.getAttribute('href')).toEqual(monitoringUrl); expect(component.$el.querySelector('.fa-area-chart')).toBeDefined(); - expect(component.$el.getAttribute('title')).toEqual('Monitoring'); + expect(component.$el.getAttribute('data-original-title')).toEqual('Monitoring'); + expect(component.$el.getAttribute('aria-label')).toEqual('Monitoring'); }); }); diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 8131f1e5b11..3f95faf466a 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -17,8 +17,15 @@ describe('Stop Component', () => { }).$mount(); }); + describe('computed', () => { + it('title', () => { + expect(component.title).toEqual('Stop'); + }); + }); + it('should render a button to stop the environment', () => { expect(component.$el.tagName).toEqual('BUTTON'); - expect(component.$el.getAttribute('title')).toEqual('Stop'); + expect(component.$el.getAttribute('data-original-title')).toEqual('Stop'); + expect(component.$el.getAttribute('aria-label')).toEqual('Stop'); }); }); diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js index 858472af4b6..f1576b19d1b 100644 --- a/spec/javascripts/environments/environment_terminal_button_spec.js +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -16,9 +16,16 @@ describe('Stop Component', () => { }).$mount(); }); + describe('computed', () => { + it('title', () => { + expect(component.title).toEqual('Terminal'); + }); + }); + it('should render a link to open a web terminal with the provided path', () => { expect(component.$el.tagName).toEqual('A'); - expect(component.$el.getAttribute('title')).toEqual('Terminal'); + expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal'); + expect(component.$el.getAttribute('aria-label')).toEqual('Terminal'); expect(component.$el.getAttribute('href')).toEqual(terminalPath); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index c92a147b937..9e2076dc383 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -4,6 +4,10 @@ import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; describe('Filtered Search Dropdown Manager', () => { + beforeEach(() => { + spyOn(jQuery, 'ajax'); + }); + describe('addWordToInput', () => { function getInputValue() { return document.querySelector('.filtered-search').value; diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 6d00d71f145..16ae649ee60 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,6 +1,7 @@ import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; +import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; import '~/lib/utils/url_utility'; import '~/lib/utils/common_utils'; import '~/filtered_search/filtered_search_token_keys'; @@ -47,18 +48,23 @@ describe('Filtered Search Manager', () => { </div> `); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + }); + + const initializeManager = () => { + /* eslint-disable jasmine/no-unsafe-spy */ spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); spyOn(gl.utils, 'getParameterByName').and.returnValue(null); spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); + /* eslint-enable jasmine/no-unsafe-spy */ input = document.querySelector('.filtered-search'); tokensContainer = document.querySelector('.tokens-container'); manager = new gl.FilteredSearchManager(); manager.setup(); - }); + }; afterEach(() => { manager.cleanup(); @@ -66,32 +72,34 @@ describe('Filtered Search Manager', () => { describe('class constructor', () => { const isLocalStorageAvailable = 'isLocalStorageAvailable'; - let filteredSearchManager; beforeEach(() => { spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable); spyOn(recentSearchesStoreSrc, 'default'); - - filteredSearchManager = new gl.FilteredSearchManager(); - filteredSearchManager.setup(); - - return filteredSearchManager; + spyOn(RecentSearchesRoot.prototype, 'render'); }); it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => { + manager = new gl.FilteredSearchManager(); + expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ isLocalStorageAvailable, allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), }); }); + }); + + describe('setup', () => { + beforeEach(() => { + manager = new gl.FilteredSearchManager(); + }); it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => { spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() => Promise.reject(new RecentSearchesServiceError())); spyOn(window, 'Flash'); - filteredSearchManager = new gl.FilteredSearchManager(); - filteredSearchManager.setup(); + manager.setup(); expect(window.Flash).not.toHaveBeenCalled(); }); @@ -100,10 +108,12 @@ describe('Filtered Search Manager', () => { describe('searchState', () => { beforeEach(() => { spyOn(gl.FilteredSearchManager.prototype, 'search').and.callFake(() => {}); + initializeManager(); }); it('should blur button', () => { const e = { + preventDefault: () => {}, currentTarget: { blur: () => {}, }, @@ -116,6 +126,7 @@ describe('Filtered Search Manager', () => { it('should not call search if there is no state', () => { const e = { + preventDefault: () => {}, currentTarget: { blur: () => {}, }, @@ -127,6 +138,7 @@ describe('Filtered Search Manager', () => { it('should call search when there is state', () => { const e = { + preventDefault: () => {}, currentTarget: { blur: () => {}, dataset: { @@ -143,6 +155,10 @@ describe('Filtered Search Manager', () => { describe('search', () => { const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; + beforeEach(() => { + initializeManager(); + }); + it('should search with a single word', (done) => { input.value = 'searchTerm'; @@ -192,6 +208,10 @@ describe('Filtered Search Manager', () => { }); describe('handleInputPlaceholder', () => { + beforeEach(() => { + initializeManager(); + }); + it('should render placeholder when there is no input', () => { expect(input.placeholder).toEqual(placeholder); }); @@ -218,6 +238,10 @@ describe('Filtered Search Manager', () => { }); describe('checkForBackspace', () => { + beforeEach(() => { + initializeManager(); + }); + describe('tokens and no input', () => { beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( @@ -255,6 +279,10 @@ describe('Filtered Search Manager', () => { }); describe('removeToken', () => { + beforeEach(() => { + initializeManager(); + }); + it('removes token even when it is already selected', () => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), @@ -286,6 +314,7 @@ describe('Filtered Search Manager', () => { describe('removeSelectedTokenKeydown', () => { beforeEach(() => { + initializeManager(); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), ); @@ -339,27 +368,39 @@ describe('Filtered Search Manager', () => { spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough(); spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough(); spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough(); - manager.removeSelectedToken(); + initializeManager(); }); it('calls FilteredSearchVisualTokens.removeSelectedToken', () => { + manager.removeSelectedToken(); + expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled(); }); it('calls handleInputPlaceholder', () => { + manager.removeSelectedToken(); + expect(manager.handleInputPlaceholder).toHaveBeenCalled(); }); it('calls toggleClearSearchButton', () => { + manager.removeSelectedToken(); + expect(manager.toggleClearSearchButton).toHaveBeenCalled(); }); it('calls update dropdown offset', () => { + manager.removeSelectedToken(); + expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled(); }); }); describe('toggleInputContainerFocus', () => { + beforeEach(() => { + initializeManager(); + }); + it('toggles on focus', () => { input.focus(); expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true); diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index a746a776548..daaddd8f390 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -55,13 +55,27 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end + it 'merge_requests/inline_changes_tab_with_comments.json' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request, action: :diffs, format: :json) + end + + it 'merge_requests/parallel_changes_tab_with_comments.json' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request, action: :diffs, format: :json, view: 'parallel') + end + private - def render_merge_request(fixture_file_name, merge_request) - get :show, + def render_merge_request(fixture_file_name, merge_request, action: :show, format: :html, view: 'inline') + get action, namespace_id: project.namespace.to_param, project_id: project, - id: merge_request.to_param + id: merge_request.to_param, + format: format, + view: view expect(response).to be_success store_frontend_fixture(response, fixture_file_name) diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml deleted file mode 100644 index ad1682704bb..00000000000 --- a/spec/javascripts/fixtures/pipelines_table.html.haml +++ /dev/null @@ -1 +0,0 @@ -#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } } diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb new file mode 100644 index 00000000000..3200577b326 --- /dev/null +++ b/spec/javascripts/fixtures/prometheus_service.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let!(:service) { create(:prometheus_service, project: project) } + + render_views + + before(:all) do + clean_frontend_fixtures('services/prometheus') + end + + before(:each) do + sign_in(admin) + end + + it 'services/prometheus/prometheus_service.html.raw' do |example| + get :edit, + namespace_id: namespace, + project_id: project, + id: service.to_param + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js index 2a77f7259da..aaffb56fa94 100644 --- a/spec/javascripts/groups/groups_spec.js +++ b/spec/javascripts/groups/groups_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import eventHub from '~/groups/event_hub'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; import groupsComponent from '~/groups/components/groups.vue'; @@ -46,6 +47,12 @@ describe('Groups Component', () => { expect(component.$el.querySelector('#group-1120')).toBeDefined(); }); + it('should respect the order of groups', () => { + const wrap = component.$el.querySelector('.groups-list-tree-container > .group-list-tree'); + expect(wrap.querySelector('.group-row:nth-child(1)').id).toBe('group-12'); + expect(wrap.querySelector('.group-row:nth-child(2)').id).toBe('group-1119'); + }); + it('should render group and its subgroup', () => { const lists = component.$el.querySelectorAll('.group-list-tree'); @@ -54,11 +61,26 @@ describe('Groups Component', () => { expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true); expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true); - expect(lists[2].querySelector('#group-1120').textContent).toContain(groups[1119].subGroups[1120].name); + expect(lists[2].querySelector('#group-1120').textContent).toContain(groups.id1119.subGroups.id1120.name); }); it('should remove prefix of parent group', () => { expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4'); }); + + it('should remove the group after leaving the group', (done) => { + spyOn(window, 'confirm').and.returnValue(true); + + eventHub.$on('leaveGroup', (group, collection) => { + store.removeGroup(group, collection); + }); + + component.$el.querySelector('#group-12 .leave-group').click(); + + Vue.nextTick(() => { + expect(component.$el.querySelector('#group-12')).toBeNull(); + done(); + }); + }); }); }); diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js index 1c0ec7a97d0..b3f5d791b89 100644 --- a/spec/javascripts/groups/mock_data.js +++ b/spec/javascripts/groups/mock_data.js @@ -6,6 +6,7 @@ const group1 = { visibility: 'public', avatar_url: null, web_url: 'http://localhost:3000/groups/level1', + group_path: '/level1', full_name: 'level1', full_path: 'level1', parent_id: null, @@ -28,6 +29,7 @@ const group14 = { visibility: 'public', avatar_url: null, web_url: 'http://localhost:3000/groups/level1/level2/level3/level4', + group_path: '/level1/level2/level3/level4', full_name: 'level1 / level2 / level3 / level4', full_path: 'level1/level2/level3/level4', parent_id: 1127, @@ -49,6 +51,7 @@ const group2 = { visibility: 'public', avatar_url: null, web_url: 'http://localhost:3000/groups/devops', + group_path: '/devops', full_name: 'devops', full_path: 'devops', parent_id: null, @@ -70,6 +73,7 @@ const group21 = { visibility: 'public', avatar_url: null, web_url: 'http://localhost:3000/groups/devops/chef', + group_path: '/devops/chef', full_name: 'devops / chef', full_path: 'devops/chef', parent_id: 1119, diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 59c006aa0af..9df92318864 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -3,17 +3,9 @@ import '~/render_math'; import '~/render_gfm'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; +import Poll from '~/lib/utils/poll'; import issueShowData from '../mock_data'; -const issueShowInterceptor = data => (request, next) => { - next(request.respondWith(JSON.stringify(data), { - status: 200, - headers: { - 'POLL-INTERVAL': 1, - }, - })); -}; - function formatText(text) { return text.trim().replace(/\s\s+/g, ' '); } @@ -24,10 +16,10 @@ describe('Issuable output', () => { let vm; beforeEach(() => { - const IssuableDescriptionComponent = Vue.extend(issuableApp); - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); - spyOn(eventHub, '$emit'); + spyOn(Poll.prototype, 'makeRequest'); + + const IssuableDescriptionComponent = Vue.extend(issuableApp); vm = new IssuableDescriptionComponent({ propsData: { @@ -51,13 +43,21 @@ describe('Issuable output', () => { }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); }); it('should render a title/description/edited and update title/description/edited on update', (done) => { - setTimeout(() => { - const editedText = vm.$el.querySelector('.edited-text'); + vm.poll.options.successCallback({ + json() { + return issueShowData.initialRequest; + }, + }); + let editedText; + Vue.nextTick() + .then(() => { + editedText = vm.$el.querySelector('.edited-text'); + }) + .then(() => { expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>'); expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>'); @@ -65,22 +65,27 @@ describe('Issuable output', () => { expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/); expect(editedText.querySelector('.author_link').href).toMatch(/\/some_user$/); expect(editedText.querySelector('time')).toBeTruthy(); - - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); - - setTimeout(() => { - expect(document.querySelector('title').innerText).toContain('2 (#1)'); - expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); - expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>'); - expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42'); - expect(vm.$el.querySelector('.edited-text')).toBeTruthy(); - expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(/Edited[\s\S]+?by Other User/); - expect(editedText.querySelector('.author_link').href).toMatch(/\/other_user$/); - expect(editedText.querySelector('time')).toBeTruthy(); - - done(); + }) + .then(() => { + vm.poll.options.successCallback({ + json() { + return issueShowData.secondRequest; + }, }); - }); + }) + .then(Vue.nextTick) + .then(() => { + expect(document.querySelector('title').innerText).toContain('2 (#1)'); + expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); + expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>'); + expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42'); + expect(vm.$el.querySelector('.edited-text')).toBeTruthy(); + expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(/Edited[\s\S]+?by Other User/); + expect(editedText.querySelector('.author_link').href).toMatch(/\/other_user$/); + expect(editedText.querySelector('time')).toBeTruthy(); + }) + .then(done) + .catch(done.fail); }); it('shows actions if permissions are correct', (done) => { @@ -126,7 +131,7 @@ describe('Issuable output', () => { describe('updateIssuable', () => { it('fetches new data after update', (done) => { - spyOn(vm.service, 'getData'); + spyOn(vm.service, 'getData').and.callThrough(); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { @@ -345,21 +350,23 @@ describe('Issuable output', () => { describe('open form', () => { it('shows locked warning if form is open & data is different', (done) => { - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); + vm.poll.options.successCallback({ + json() { + return issueShowData.initialRequest; + }, + }); Vue.nextTick() - .then(() => new Promise((resolve) => { - setTimeout(resolve); - })) .then(() => { vm.openForm(); - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); - - return new Promise((resolve) => { - setTimeout(resolve); + vm.poll.options.successCallback({ + json() { + return issueShowData.secondRequest; + }, }); }) + .then(Vue.nextTick) .then(() => { expect( vm.formState.lockedWarningVisible, @@ -368,9 +375,8 @@ describe('Issuable output', () => { expect( vm.$el.querySelector('.alert'), ).not.toBeNull(); - - done(); }) + .then(done) .catch(done.fail); }); }); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 408349cc42d..f3fdbff01a6 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -95,5 +95,33 @@ describe('Description component', () => { done(); }); }); + + it('clears task status text when no tasks are present', (done) => { + vm.taskStatus = '0 of 0'; + + setTimeout(() => { + expect( + document.querySelector('.issuable-meta #task_status').textContent.trim(), + ).toBe(''); + + done(); + }); + }); + }); + + it('applies syntax highlighting and math when description changed', (done) => { + spyOn(vm, 'renderGFM').and.callThrough(); + spyOn($.prototype, 'renderGFM').and.callThrough(); + vm.descriptionHtml = 'changed'; + + Vue.nextTick(() => { + setTimeout(() => { + expect(vm.$refs['gfm-content']).toBeDefined(); + expect(vm.renderGFM).toHaveBeenCalled(); + expect($.prototype.renderGFM).toHaveBeenCalled(); + + done(); + }); + }); }); }); diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js index f5b35b1e8b0..df8189d9290 100644 --- a/spec/javascripts/issue_show/components/fields/description_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_spec.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import eventHub from '~/issue_show/event_hub'; import Store from '~/issue_show/stores'; import descriptionField from '~/issue_show/components/fields/description.vue'; +import { keyboardDownEvent } from '../../helpers'; describe('Description field component', () => { let vm; @@ -18,6 +20,8 @@ describe('Description field component', () => { document.body.appendChild(el); + spyOn(eventHub, '$emit'); + vm = new Component({ el, propsData: { @@ -53,4 +57,20 @@ describe('Description field component', () => { document.activeElement, ).toBe(vm.$refs.textarea); }); + + it('triggers update with meta+enter', () => { + vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); + + it('triggers update with ctrl+enter', () => { + vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); }); diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js index 53ae038a6a2..a03b462689f 100644 --- a/spec/javascripts/issue_show/components/fields/title_spec.js +++ b/spec/javascripts/issue_show/components/fields/title_spec.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import eventHub from '~/issue_show/event_hub'; import Store from '~/issue_show/stores'; import titleField from '~/issue_show/components/fields/title.vue'; +import { keyboardDownEvent } from '../../helpers'; describe('Title field component', () => { let vm; @@ -15,6 +17,8 @@ describe('Title field component', () => { }); store.formState.title = 'test'; + spyOn(eventHub, '$emit'); + vm = new Component({ propsData: { formState: store.formState, @@ -27,4 +31,20 @@ describe('Title field component', () => { vm.$el.querySelector('.form-control').value, ).toBe('test'); }); + + it('triggers update with meta+enter', () => { + vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); + + it('triggers update with ctrl+enter', () => { + vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); }); diff --git a/spec/javascripts/issue_show/helpers.js b/spec/javascripts/issue_show/helpers.js new file mode 100644 index 00000000000..5d2ced98ae4 --- /dev/null +++ b/spec/javascripts/issue_show/helpers.js @@ -0,0 +1,10 @@ +// eslint-disable-next-line import/prefer-default-export +export const keyboardDownEvent = (code, metaKey = false, ctrlKey = false) => { + const e = new CustomEvent('keydown'); + + e.keyCode = code; + e.metaKey = metaKey; + e.ctrlKey = ctrlKey; + + return e; +}; diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index e54acfa8e44..9e9eb17d439 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -7,54 +7,92 @@ import '~/render_gfm'; import '~/render_math'; import '~/notes'; +const upArrowKeyCode = 38; + describe('Merge request notes', () => { window.gon = window.gon || {}; window.gl = window.gl || {}; gl.utils = gl.utils || {}; - const fixture = 'merge_requests/diff_comment.html.raw'; - preloadFixtures(fixture); + const discussionTabFixture = 'merge_requests/diff_comment.html.raw'; + const changesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; + preloadFixtures(discussionTabFixture, changesTabJsonFixture); - beforeEach(() => { - loadFixtures(fixture); - gl.utils.disableButtonIfEmptyField = _.noop; - window.project_uploads_path = 'http://test.host/uploads'; - $('body').data('page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('author-id'); + describe('Discussion tab with diff comments', () => { + beforeEach(() => { + loadFixtures(discussionTabFixture); + gl.utils.disableButtonIfEmptyField = _.noop; + window.project_uploads_path = 'http://test.host/uploads'; + $('body').data('page', 'projects:merge_requests:show'); + window.gon.current_user_id = $('.note:last').data('author-id'); - return new Notes('', []); - }); + return new Notes('', []); + }); + + describe('up arrow', () => { + it('edits last comment when triggered in main form', () => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = upArrowKeyCode; + + spyOnEvent('.note:last .js-note-edit', 'click'); + + $('.js-note-text').trigger(upArrowEvent); + + expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); + }); + + it('edits last comment in discussion when triggered in discussion form', (done) => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = upArrowKeyCode; + + spyOnEvent('.note-discussion .js-note-edit', 'click'); + + $('.js-discussion-reply-button').click(); - describe('up arrow', () => { - it('edits last comment when triggered in main form', () => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = 38; + setTimeout(() => { + expect( + $('.note-discussion .js-note-text'), + ).toExist(); - spyOnEvent('.note:last .js-note-edit', 'click'); + $('.note-discussion .js-note-text').trigger(upArrowEvent); - $('.js-note-text').trigger(upArrowEvent); + expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit'); - expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); + done(); + }); + }); }); + }); - it('edits last comment in discussion when triggered in discussion form', (done) => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = 38; + describe('Changes tab with diff comments', () => { + beforeEach(() => { + const diffsResponse = getJSONFixture(changesTabJsonFixture); + const noteFormHtml = `<form class="js-new-note-form"> + <textarea class="js-note-text"></textarea> + </form>`; + setFixtures(diffsResponse.html + noteFormHtml); + $('body').data('page', 'projects:merge_requests:show'); + window.gon.current_user_id = $('.note:last').data('author-id'); + + return new Notes('', []); + }); - spyOnEvent('.note-discussion .js-note-edit', 'click'); + describe('up arrow', () => { + it('edits last comment in discussion when triggered in discussion form', (done) => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = upArrowKeyCode; - $('.js-discussion-reply-button').click(); + spyOnEvent('.note:last .js-note-edit', 'click'); - setTimeout(() => { - expect( - $('.note-discussion .js-note-text'), - ).toExist(); + $('.js-discussion-reply-button').trigger('click'); - $('.note-discussion .js-note-text').trigger(upArrowEvent); + setTimeout(() => { + $('.js-note-text').trigger(upArrowEvent); - expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit'); + expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); - done(); + done(); + }); }); }); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7b910282cc8..bb6b5d852d3 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -12,15 +12,6 @@ import '~/notes'; import 'vendor/jquery.scrollTo'; (function () { - // TODO: remove this hack! - // PhantomJS causes spyOn to panic because replaceState isn't "writable" - var phantomjs; - try { - phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; - } catch (err) { - phantomjs = false; - } - describe('MergeRequestTabs', function () { var stubLocation = {}; var setLocation = function (stubs) { @@ -31,17 +22,23 @@ import 'vendor/jquery.scrollTo'; }; $.extend(stubLocation, defaults, stubs || {}); }; - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw'); + + const inlineChangesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; + const parallelChangesTabJsonFixture = 'merge_requests/parallel_changes_tab_with_comments.json'; + preloadFixtures( + 'merge_requests/merge_request_with_task_list.html.raw', + 'merge_requests/diff_comment.html.raw', + inlineChangesTabJsonFixture, + parallelChangesTabJsonFixture + ); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); setLocation(); - if (!phantomjs) { - this.spies = { - history: spyOn(window.history, 'replaceState').and.callFake(function () {}) - }; - } + this.spies = { + history: spyOn(window.history, 'replaceState').and.callFake(function () {}) + }; }); afterEach(function () { @@ -208,11 +205,9 @@ import 'vendor/jquery.scrollTo'; pathname: '/foo/bar/merge_requests/1' }); newState = this.subject('commits'); - if (!phantomjs) { - expect(this.spies.history).toHaveBeenCalledWith({ - url: newState - }, document.title, newState); - } + expect(this.spies.history).toHaveBeenCalledWith({ + url: newState + }, document.title, newState); }); it('treats "show" like "notes"', function () { @@ -284,6 +279,19 @@ import 'vendor/jquery.scrollTo'; }); describe('loadDiff', function () { + beforeEach(() => { + loadFixtures('merge_requests/diff_comment.html.raw'); + spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); + window.gl.ImageFile = () => {}; + window.notes = new Notes('', []); + spyOn(window.notes, 'toggleDiffNote').and.callThrough(); + }); + + afterEach(() => { + delete window.gl.ImageFile; + delete window.notes; + }); + it('requires an absolute pathname', function () { spyOn($, 'ajax').and.callFake(function (options) { expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json'); @@ -292,43 +300,112 @@ import 'vendor/jquery.scrollTo'; this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); }); - describe('with note fragment hash', () => { + describe('with inline diff', () => { + let noteId; + let noteLineNumId; + beforeEach(() => { - loadFixtures('merge_requests/diff_comment.html.raw'); - spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); - window.notes = new Notes('', []); - spyOn(window.notes, 'toggleDiffNote').and.callThrough(); - }); + const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture); + + const $html = $(diffsResponse.html); + noteId = $html.find('.note').attr('id'); + noteLineNumId = $html + .find('.note') + .closest('.notes_holder') + .prev('.line_holder') + .find('a[data-linenumber]') + .attr('href') + .replace('#', ''); - afterEach(() => { - delete window.notes; + spyOn($, 'ajax').and.callFake(function (options) { + options.success(diffsResponse); + }); }); - it('should expand and scroll to linked fragment hash #note_xxx', function () { - const noteId = 'note_1'; - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ html: `<div id="${noteId}">foo</div>` }); + describe('with note fragment hash', () => { + it('should expand and scroll to linked fragment hash #note_xxx', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(noteId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'old', + forceShow: true, + }); }); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + it('should gracefully ignore non-existant fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'old', - forceShow: true, + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); }); }); - it('should gracefully ignore non-existant fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + describe('with line number fragment hash', () => { + it('should gracefully ignore line number fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(noteLineNumId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); + }); + }); + + describe('with parallel diff', () => { + let noteId; + let noteLineNumId; + + beforeEach(() => { + const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture); + + const $html = $(diffsResponse.html); + noteId = $html.find('.note').attr('id'); + noteLineNumId = $html + .find('.note') + .closest('.notes_holder') + .prev('.line_holder') + .find('a[data-linenumber]') + .attr('href') + .replace('#', ''); + spyOn($, 'ajax').and.callFake(function (options) { - options.success({ html: '' }); + options.success(diffsResponse); + }); + }); + + describe('with note fragment hash', () => { + it('should expand and scroll to linked fragment hash #note_xxx', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(noteId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'new', + forceShow: true, + }); + }); + + it('should gracefully ignore non-existant fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); }); + }); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + describe('with line number fragment hash', () => { + it('should gracefully ignore line number fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + expect(noteLineNumId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index bfd8b8648a6..5ece4ed080b 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -126,6 +126,7 @@ import '~/notes'; const deferred = $.Deferred(); spyOn($, 'ajax').and.returnValue(deferred.promise()); spyOn(this.notes, 'revertNoteEditForm'); + spyOn(this.notes, 'setupNewNote'); $('.js-comment-button').click(); deferred.resolve(noteEntity); @@ -136,6 +137,46 @@ import '~/notes'; this.notes.updateNote(updatedNote, $targetNote); expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); + expect(this.notes.setupNewNote).toHaveBeenCalled(); + }); + }); + + describe('updateNoteTargetSelector', () => { + const hash = 'note_foo'; + let $note; + + beforeEach(() => { + $note = $(`<div id="${hash}"></div>`); + spyOn($note, 'filter').and.callThrough(); + spyOn($note, 'toggleClass').and.callThrough(); + }); + + it('sets target when hash matches', () => { + spyOn(gl.utils, 'getLocationHash'); + gl.utils.getLocationHash.and.returnValue(hash); + + Notes.updateNoteTargetSelector($note); + + expect($note.filter).toHaveBeenCalledWith(`#${hash}`); + expect($note.toggleClass).toHaveBeenCalledWith('target', true); + }); + + it('unsets target when hash does not match', () => { + spyOn(gl.utils, 'getLocationHash'); + gl.utils.getLocationHash.and.returnValue('note_doesnotexist'); + + Notes.updateNoteTargetSelector($note); + + expect($note.toggleClass).toHaveBeenCalledWith('target', false); + }); + + it('unsets target when there is not a hash fragment anymore', () => { + spyOn(gl.utils, 'getLocationHash'); + gl.utils.getLocationHash.and.returnValue(null); + + Notes.updateNoteTargetSelector($note); + + expect($note.toggleClass).toHaveBeenCalledWith('target', false); }); }); @@ -189,9 +230,13 @@ import '~/notes'; Notes.isUpdatedNote.and.returnValue(true); const $note = $('<div>'); $notesList.find.and.returnValue($note); + const $newNote = $(note.html); + Notes.animateUpdateNote.and.returnValue($newNote); + Notes.prototype.renderNote.call(notes, note, null, $notesList); expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note); + expect(notes.setupNewNote).toHaveBeenCalledWith($newNote); }); describe('while editing', () => { @@ -378,6 +423,23 @@ import '~/notes'; }); }); + describe('putEditFormInPlace', () => { + it('should call gl.GLForm with GFM parameter passed through', () => { + spyOn(gl, 'GLForm'); + + const $el = jasmine.createSpyObj('$form', ['find', 'closest']); + $el.find.and.returnValue($('<div>')); + $el.closest.and.returnValue($('<div>')); + + Notes.prototype.putEditFormInPlace.call({ + getEditFormSelector: () => '', + enableGFM: true + }, $el); + + expect(gl.GLForm).toHaveBeenCalledWith(jasmine.any(Object), true); + }); + }); + describe('postComment & updateComment', () => { const sampleComment = 'foo'; const updatedComment = 'bar'; @@ -533,46 +595,46 @@ import '~/notes'; }); }); - describe('hasSlashCommands', () => { + describe('hasQuickActions', () => { beforeEach(() => { this.notes = new Notes('', []); }); - it('should return true when comment begins with a slash command', () => { + it('should return true when comment begins with a quick action', () => { const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; - const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); + const hasQuickActions = this.notes.hasQuickActions(sampleComment); - expect(hasSlashCommands).toBeTruthy(); + expect(hasQuickActions).toBeTruthy(); }); - it('should return false when comment does NOT begin with a slash command', () => { + it('should return false when comment does NOT begin with a quick action', () => { const sampleComment = 'Hey, /unassign Merging this'; - const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); + const hasQuickActions = this.notes.hasQuickActions(sampleComment); - expect(hasSlashCommands).toBeFalsy(); + expect(hasQuickActions).toBeFalsy(); }); - it('should return false when comment does NOT have any slash commands', () => { + it('should return false when comment does NOT have any quick actions', () => { const sampleComment = 'Looking good, Awesome!'; - const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); + const hasQuickActions = this.notes.hasQuickActions(sampleComment); - expect(hasSlashCommands).toBeFalsy(); + expect(hasQuickActions).toBeFalsy(); }); }); - describe('stripSlashCommands', () => { - it('should strip slash commands from the comment which begins with a slash command', () => { + describe('stripQuickActions', () => { + it('should strip quick actions from the comment which begins with a quick action', () => { this.notes = new Notes(); const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; - const stripedComment = this.notes.stripSlashCommands(sampleComment); + const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe(''); }); - it('should strip slash commands from the comment but leaves plain comment if it is present', () => { + it('should strip quick actions from the comment but leaves plain comment if it is present', () => { this.notes = new Notes(); const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this'; - const stripedComment = this.notes.stripSlashCommands(sampleComment); + const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe('Merging this'); }); @@ -580,14 +642,14 @@ import '~/notes'; it('should NOT strip string that has slashes within', () => { this.notes = new Notes(); const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1'; - const stripedComment = this.notes.stripSlashCommands(sampleComment); + const stripedComment = this.notes.stripQuickActions(sampleComment); expect(stripedComment).toBe(sampleComment); }); }); - describe('getSlashCommandDescription', () => { - const availableSlashCommands = [ + describe('getQuickActionDescription', () => { + const availableQuickActions = [ { name: 'close', description: 'Close this issue', params: [] }, { name: 'title', description: 'Change title', params: [{}] }, { name: 'estimate', description: 'Set time estimate', params: [{}] } @@ -597,19 +659,19 @@ import '~/notes'; this.notes = new Notes(); }); - it('should return executing slash command description when note has single slash command', () => { + it('should return executing quick action description when note has single quick action', () => { const sampleComment = '/close'; - expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying command to close this issue'); + expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying command to close this issue'); }); - it('should return generic multiple slash command description when note has multiple slash commands', () => { + it('should return generic multiple quick action description when note has multiple quick actions', () => { const sampleComment = '/close\n/title [Duplicate] Issue foobar'; - expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying multiple commands'); + expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying multiple commands'); }); - it('should return generic slash command description when available slash commands list is not populated', () => { + it('should return generic quick action description when available quick actions list is not populated', () => { const sampleComment = '/close\n/title [Duplicate] Issue foobar'; - expect(this.notes.getSlashCommandDescription(sampleComment)).toBe('Applying command'); + expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command'); }); }); diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js index 845b371d90c..040d14efed2 100644 --- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js +++ b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js @@ -1,5 +1,8 @@ import Vue from 'vue'; -import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input'; +import Translate from '~/vue_shared/translate'; +import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input.vue'; + +Vue.use(Translate); const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput); const inputNameAttribute = 'schedule[cron]'; @@ -95,7 +98,7 @@ describe('Interval Pattern Input Component', function () { describe('User Actions', function () { beforeEach(function () { - // For an unknown reason, Phantom.js doesn't trigger click events + // For an unknown reason, some browsers do not propagate click events // on radio buttons in a way Vue can register. So, we have to mount // to a fixture. setFixtures('<div id="my-mount"></div>'); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 28c9c7ab282..48620898357 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -1,25 +1,20 @@ import Vue from 'vue'; import asyncButtonComp from '~/pipelines/components/async_button.vue'; +import eventHub from '~/pipelines/event_hub'; describe('Pipelines Async Button', () => { let component; - let spy; let AsyncButtonComponent; beforeEach(() => { AsyncButtonComponent = Vue.extend(asyncButtonComp); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - component = new AsyncButtonComponent({ propsData: { endpoint: '/foo', title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - service: { - postAction: spy, - }, }, }).$mount(); }); @@ -33,7 +28,7 @@ describe('Pipelines Async Button', () => { }); it('should render the provided title', () => { - expect(component.$el.getAttribute('title')).toContain('Foo'); + expect(component.$el.getAttribute('data-original-title')).toContain('Foo'); expect(component.$el.getAttribute('aria-label')).toContain('Foo'); }); @@ -41,37 +36,12 @@ describe('Pipelines Async Button', () => { expect(component.$el.getAttribute('class')).toContain('bar'); }); - it('should call the service when it is clicked with the provided endpoint', () => { - component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); - }); - - it('should hide loading if request fails', () => { - spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'fa fa-foo', - cssClass: 'bar', - dataAttributes: { - 'data-foo': 'foo', - }, - service: { - postAction: spy, - }, - }, - }).$mount(); - - component.$el.click(); - expect(component.$el.querySelector('.fa-spinner')).toBe(null); - }); - describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { spyOn(window, 'confirm').and.returnValue(true); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + eventHub.$on('postAction', (endpoint) => { + expect(endpoint).toEqual('/foo'); + }); component = new AsyncButtonComponent({ propsData: { @@ -79,15 +49,11 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - service: { - postAction: spy, - }, confirmActionMessage: 'bar', }, }).$mount(); component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); }); }); }); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 594a9856d2c..3c4b20a5f06 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -19,7 +19,7 @@ describe('Pipeline Url Component', () => { }, }).$mount(); - expect(component.$el.tagName).toEqual('TD'); + expect(component.$el.getAttribute('class')).toContain('table-section'); }); it('should render a link the provided path and id', () => { @@ -94,7 +94,7 @@ describe('Pipeline Url Component', () => { }, }).$mount(); - expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest'); + expect(component.$el.querySelector('.js-pipeline-url-latest').textContent).toContain('latest'); expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid'); expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); }); diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index c89dacbcd93..72fb0a8f9ef 100644 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -1,9 +1,8 @@ import Vue from 'vue'; -import pipelinesActionsComp from '~/pipelines/components/pipelines_actions'; +import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue'; describe('Pipelines Actions dropdown', () => { let component; - let spy; let actions; let ActionsComponent; @@ -22,14 +21,9 @@ describe('Pipelines Actions dropdown', () => { }, ]; - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - component = new ActionsComponent({ propsData: { actions, - service: { - postAction: spy, - }, }, }).$mount(); }); @@ -40,31 +34,6 @@ describe('Pipelines Actions dropdown', () => { ).toEqual(actions.length); }); - it('should call the service when an action is clicked', () => { - component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); - component.$el.querySelector('.js-pipeline-action-link').click(); - - expect(spy).toHaveBeenCalledWith(actions[0].path); - }); - - it('should hide loading if request fails', () => { - spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); - - component = new ActionsComponent({ - propsData: { - actions, - service: { - postAction: spy, - }, - }, - }).$mount(); - - component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); - component.$el.querySelector('.js-pipeline-action-link').click(); - - expect(component.$el.querySelector('.fa-spinner')).toEqual(null); - }); - it('should render a disabled action when it\'s not playable', () => { expect( component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js index 9724b63d957..acb67d0ec21 100644 --- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js +++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import artifactsComp from '~/pipelines/components/pipelines_artifacts'; +import artifactsComp from '~/pipelines/components/pipelines_artifacts.vue'; describe('Pipelines Artifacts dropdown', () => { let component; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 3a56156358b..c30abb2edb0 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesComp from '~/pipelines/pipelines'; +import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; describe('Pipelines', () => { diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 67419cfcbea..7ce39dca112 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import tableRowComp from '~/vue_shared/components/pipelines_table_row'; +import tableRowComp from '~/pipelines/components/pipelines_table_row.vue'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -34,7 +34,7 @@ describe('Pipelines Table Row', () => { it('should render a table row', () => { component = buildComponent(pipeline); - expect(component.$el).toEqual('TR'); + expect(component.$el.getAttribute('class')).toContain('gl-responsive-table-row'); }); describe('status column', () => { @@ -44,13 +44,13 @@ describe('Pipelines Table Row', () => { it('should render a pipeline link', () => { expect( - component.$el.querySelector('td.commit-link a').getAttribute('href'), + component.$el.querySelector('.table-section.commit-link a').getAttribute('href'), ).toEqual(pipeline.path); }); it('should render status text', () => { expect( - component.$el.querySelector('td.commit-link a').textContent, + component.$el.querySelector('.table-section.commit-link a').textContent, ).toContain(pipeline.details.status.text); }); }); @@ -62,24 +62,24 @@ describe('Pipelines Table Row', () => { it('should render a pipeline link', () => { expect( - component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), + component.$el.querySelector('.table-section:nth-child(2) a').getAttribute('href'), ).toEqual(pipeline.path); }); it('should render pipeline ID', () => { expect( - component.$el.querySelector('td:nth-child(2) a > span').textContent, + component.$el.querySelector('.table-section:nth-child(2) a > span').textContent, ).toEqual(`#${pipeline.id}`); }); describe('when a user is provided', () => { it('should render user information', () => { expect( - component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), + component.$el.querySelector('.table-section:nth-child(2) a:nth-child(3)').getAttribute('href'), ).toEqual(pipeline.user.path); expect( - component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'), + component.$el.querySelector('.table-section:nth-child(2) img').getAttribute('data-original-title'), ).toEqual(pipeline.user.name); }); }); @@ -142,7 +142,7 @@ describe('Pipelines Table Row', () => { it('should render an icon for each stage', () => { expect( - component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, + component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button').length, ).toEqual(pipeline.details.stages.length); }); }); @@ -154,7 +154,7 @@ describe('Pipelines Table Row', () => { it('should render the provided actions', () => { expect( - component.$el.querySelectorAll('td:nth-child(6) ul li').length, + component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length, ).toEqual(pipeline.details.manual_actions.length); }); }); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index 6cc178b8f1d..3afe89c8db4 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesTableComp from '~/vue_shared/components/pipelines_table'; +import pipelinesTableComp from '~/pipelines/components/pipelines_table.vue'; import '~/lib/utils/datetime_utility'; describe('Pipelines Table', () => { @@ -22,7 +22,6 @@ describe('Pipelines Table', () => { component = new PipelinesTableComponent({ propsData: { pipelines: [], - service: {}, }, }).$mount(); }); @@ -32,16 +31,14 @@ describe('Pipelines Table', () => { }); it('should render a table', () => { - expect(component.$el).toEqual('TABLE'); + expect(component.$el.getAttribute('class')).toContain('ci-table'); }); it('should render table head with correct columns', () => { - expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status'); - expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline'); - expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit'); - expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages'); - expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual(''); - expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual(''); + expect(component.$el.querySelector('.table-section.js-pipeline-status').textContent.trim()).toEqual('Status'); + expect(component.$el.querySelector('.table-section.js-pipeline-info').textContent.trim()).toEqual('Pipeline'); + expect(component.$el.querySelector('.table-section.js-pipeline-commit').textContent.trim()).toEqual('Commit'); + expect(component.$el.querySelector('.table-section.js-pipeline-stages').textContent.trim()).toEqual('Stages'); }); }); @@ -50,24 +47,21 @@ describe('Pipelines Table', () => { const component = new PipelinesTableComponent({ propsData: { pipelines: [], - service: {}, }, }).$mount(); - expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); + expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0); }); }); describe('with data', () => { it('should render rows', () => { const component = new PipelinesTableComponent({ - el: document.querySelector('.test-dom-element'), propsData: { pipelines: [pipeline], - service: {}, }, }).$mount(); - expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); + expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(1); }); }); }); diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index a4f32a1faed..1b96b2e3d51 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -83,4 +83,47 @@ describe('Pipelines stage component', () => { }, 0); }); }); + + describe('update endpoint correctly', () => { + const updatedInterceptor = (request, next) => { + if (request.url === 'bar') { + next(request.respondWith(JSON.stringify({ html: 'this is the updated content' }), { + status: 200, + })); + } + next(); + }; + + beforeEach(() => { + Vue.http.interceptors.push(updatedInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, updatedInterceptor, + ); + }); + + it('should update the stage to request the new endpoint provided', (done) => { + component.stage = { + status: { + group: 'running', + icon: 'running', + title: 'running', + }, + dropdown_path: 'bar', + }; + + Vue.nextTick(() => { + component.$el.querySelector('button').click(); + + setTimeout(() => { + expect( + component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(), + ).toEqual('this is the updated content'); + done(); + }); + }); + }); + }); }); diff --git a/spec/javascripts/pipelines/time_ago_spec.js b/spec/javascripts/pipelines/time_ago_spec.js index 24581e8c672..42b34c82f89 100644 --- a/spec/javascripts/pipelines/time_ago_spec.js +++ b/spec/javascripts/pipelines/time_ago_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import timeAgo from '~/pipelines/components/time_ago'; +import timeAgo from '~/pipelines/components/time_ago.vue'; describe('Timeago component', () => { let TimeAgo; diff --git a/spec/javascripts/pipelines_spec.js b/spec/javascripts/pipelines_spec.js index 81ac589f4e6..c08a73851be 100644 --- a/spec/javascripts/pipelines_spec.js +++ b/spec/javascripts/pipelines_spec.js @@ -1,10 +1,5 @@ import Pipelines from '~/pipelines'; -// Fix for phantomJS -if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) { - Element.prototype.matches = Element.prototype.webkitMatchesSelector; -} - describe('Pipelines', () => { preloadFixtures('static/pipeline_graph.html.raw'); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 3dba2e817ff..cc336180ff7 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,4 +1,3 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */ /* global Project */ import 'select2/select2'; @@ -7,47 +6,52 @@ import '~/api'; import '~/project_select'; import '~/project'; -(function() { - describe('Project Title', function() { - preloadFixtures('issues/open-issue.html.raw'); - loadJSONFixtures('projects.json'); +describe('Project Title', () => { + preloadFixtures('issues/open-issue.html.raw'); + loadJSONFixtures('projects.json'); - beforeEach(function() { - loadFixtures('issues/open-issue.html.raw'); + beforeEach(() => { + loadFixtures('issues/open-issue.html.raw'); - window.gon = {}; - window.gon.api_version = 'v3'; + window.gon = {}; + window.gon.api_version = 'v3'; - return this.project = new Project(); - }); + // eslint-disable-next-line no-new + new Project(); + }); - describe('project list', function() { - var fakeAjaxResponse = function fakeAjaxResponse(req) { - var d; - expect(req.url).toBe('/api/v3/projects.json?simple=true'); - expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true }); - d = $.Deferred(); - d.resolve(this.projects_data); - return d.promise(); - }; - - beforeEach((function(_this) { - return function() { - _this.projects_data = getJSONFixture('projects.json'); - return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); - }; - })(this)); - it('toggles dropdown', function() { - var menu = $('.js-dropdown-menu-projects'); - $('.js-projects-dropdown-toggle').click(); - expect(menu).toHaveClass('open'); - menu.find('.dropdown-menu-close-icon').click(); - expect(menu).not.toHaveClass('open'); + describe('project list', () => { + let reqUrl; + let reqData; + + beforeEach(() => { + const fakeResponseData = getJSONFixture('projects.json'); + spyOn(jQuery, 'ajax').and.callFake((req) => { + const def = $.Deferred(); + reqUrl = req.url; + reqData = req.data; + def.resolve(fakeResponseData); + return def.promise(); }); }); - afterEach(() => { - window.gon = {}; + it('toggles dropdown', () => { + const $menu = $('.js-dropdown-menu-projects'); + $('.js-projects-dropdown-toggle').click(); + expect($menu).toHaveClass('open'); + expect(reqUrl).toBe('/api/v3/projects.json?simple=true'); + expect(reqData).toEqual({ + search: '', + order_by: 'last_activity_at', + per_page: 20, + membership: true, + }); + $menu.find('.dropdown-menu-close-icon').click(); + expect($menu).not.toHaveClass('open'); }); }); -}).call(window); + + afterEach(() => { + window.gon = {}; + }); +}); diff --git a/spec/javascripts/prometheus_metrics/mock_data.js b/spec/javascripts/prometheus_metrics/mock_data.js new file mode 100644 index 00000000000..3af56df92e2 --- /dev/null +++ b/spec/javascripts/prometheus_metrics/mock_data.js @@ -0,0 +1,41 @@ +export const metrics = [ + { + group: 'Kubernetes', + priority: 1, + active_metrics: 4, + metrics_missing_requirements: 0, + }, + { + group: 'HAProxy', + priority: 2, + active_metrics: 3, + metrics_missing_requirements: 0, + }, + { + group: 'Apache', + priority: 3, + active_metrics: 5, + metrics_missing_requirements: 0, + }, +]; + +export const missingVarMetrics = [ + { + group: 'Kubernetes', + priority: 1, + active_metrics: 4, + metrics_missing_requirements: 0, + }, + { + group: 'HAProxy', + priority: 2, + active_metrics: 3, + metrics_missing_requirements: 1, + }, + { + group: 'Apache', + priority: 3, + active_metrics: 5, + metrics_missing_requirements: 3, + }, +]; diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js new file mode 100644 index 00000000000..2b3a821dbd9 --- /dev/null +++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js @@ -0,0 +1,158 @@ +import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; +import PANEL_STATE from '~/prometheus_metrics/constants'; +import { metrics, missingVarMetrics } from './mock_data'; + +describe('PrometheusMetrics', () => { + const FIXTURE = 'services/prometheus/prometheus_service.html.raw'; + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + }); + + describe('constructor', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should initialize wrapper element refs on class object', () => { + expect(prometheusMetrics.$wrapper).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsPanel).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsCount).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsLoading).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsEmpty).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsList).toBeDefined(); + expect(prometheusMetrics.$missingEnvVarPanel).toBeDefined(); + expect(prometheusMetrics.$panelToggle).toBeDefined(); + expect(prometheusMetrics.$missingEnvVarMetricCount).toBeDefined(); + expect(prometheusMetrics.$missingEnvVarMetricsList).toBeDefined(); + }); + + it('should initialize metadata on class object', () => { + expect(prometheusMetrics.backOffRequestCounter).toEqual(0); + expect(prometheusMetrics.activeMetricsEndpoint).toContain('/test'); + }); + }); + + describe('showMonitoringMetricsPanelState', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should show loading state when called with `loading`', () => { + prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + }); + + it('should show metrics list when called with `list`', () => { + prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + }); + + it('should show empty state when called with `empty`', () => { + prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + }); + }); + + describe('populateActiveMetrics', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should show monitored metrics list', () => { + prometheusMetrics.populateActiveMetrics(metrics); + + const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li'); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + + expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual('12'); + expect($metricsListLi.length).toEqual(metrics.length); + expect($metricsListLi.first().find('.badge').text()).toEqual(`${metrics[0].active_metrics}`); + }); + + it('should show missing environment variables list', () => { + prometheusMetrics.populateActiveMetrics(missingVarMetrics); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy(); + + expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2'); + expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2); + expect(prometheusMetrics.$missingEnvVarPanel.find('.flash-container')).toBeDefined(); + }); + }); + + describe('loadActiveMetrics', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should show loader animation while response is being loaded and hide it when request is complete', (done) => { + const deferred = $.Deferred(); + spyOn($, 'getJSON').and.returnValue(deferred.promise()); + + prometheusMetrics.loadActiveMetrics(); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); + expect($.getJSON).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); + + deferred.resolve({ data: metrics, success: true }); + + setTimeout(() => { + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + done(); + }); + }); + + it('should show empty state if response failed to load', (done) => { + const deferred = $.Deferred(); + spyOn($, 'getJSON').and.returnValue(deferred.promise()); + spyOn(prometheusMetrics, 'populateActiveMetrics'); + + prometheusMetrics.loadActiveMetrics(); + + deferred.reject(); + + setTimeout(() => { + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); + done(); + }); + }); + + it('should populate metrics list once response is loaded', (done) => { + const deferred = $.Deferred(); + spyOn($, 'getJSON').and.returnValue(deferred.promise()); + spyOn(prometheusMetrics, 'populateActiveMetrics'); + + prometheusMetrics.loadActiveMetrics(); + + deferred.resolve({ data: metrics, success: true }); + + setTimeout(() => { + expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js index 5b5b1bf4140..ac93f918ce4 100644 --- a/spec/javascripts/sidebar/assignee_title_spec.js +++ b/spec/javascripts/sidebar/assignee_title_spec.js @@ -33,6 +33,31 @@ describe('AssigneeTitle component', () => { }); }); + describe('gutter toggle', () => { + it('does not show toggle by default', () => { + component = new AssigneeTitleComponent({ + propsData: { + numberOfAssignees: 2, + editable: false, + }, + }).$mount(); + + expect(component.$el.querySelector('.gutter-toggle')).toBeNull(); + }); + + it('shows toggle when showToggle is true', () => { + component = new AssigneeTitleComponent({ + propsData: { + numberOfAssignees: 2, + editable: false, + showToggle: true, + }, + }).$mount(); + + expect(component.$el.querySelector('.gutter-toggle')).toEqual(jasmine.any(Object)); + }); + }); + it('does not render spinner by default', () => { component = new AssigneeTitleComponent({ propsData: { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 2c34402576b..d4e134583c7 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,8 +1,18 @@ +/* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; import _ from 'underscore'; import 'jasmine-jquery'; import '~/commons'; +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent); +Vue.config.devtools = !isHeadlessChrome; +Vue.config.productionTip = false; + +Vue.use(VueResource); + // enable test fixtures jasmine.getFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; @@ -16,6 +26,45 @@ window.gl = window.gl || {}; window.gl.TEST_HOST = 'http://test.host'; window.gon = window.gon || {}; +let hasUnhandledPromiseRejections = false; + +window.addEventListener('unhandledrejection', (event) => { + hasUnhandledPromiseRejections = true; + console.error('Unhandled promise rejection:'); + console.error(event.reason.stack || event.reason); +}); + +const checkUnhandledPromiseRejections = (done) => { + expect(hasUnhandledPromiseRejections).toBe(false); + done(); +}; + +// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row +// because it appears to lock up the thread that communicates to Karma's socket +// This async beforeEach gets called on every spec and releases the JS thread long +// enough for the socket to continue to communicate. +// The downside is that it creates a minor performance penalty in the time it takes +// to run our unit tests. +beforeEach(done => done()); + +beforeAll(() => { + const origError = console.error; + spyOn(console, 'error').and.callFake((message) => { + if (/^\[Vue warn\]/.test(message)) { + fail(message); + } else { + origError(message); + } + }); +}); + +const builtinVueHttpInterceptors = Vue.http.interceptors.slice(); + +beforeEach(() => { + // restore interceptors so we have no remaining ones from previous tests + Vue.http.interceptors = builtinVueHttpInterceptors.slice(); +}); + // render all of our tests const testsContext = require.context('.', true, /_spec$/); testsContext.keys().forEach(function (path) { @@ -31,6 +80,10 @@ testsContext.keys().forEach(function (path) { } }); +it('has no unhandled Promise rejections', (done) => { + setTimeout(checkUnhandledPromiseRejections(done), 1000); +}); + // if we're generating coverage reports, make sure to include all files so // that we can catch files with 0% coverage // see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index 647b59520f8..4b6f171c8d6 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -76,6 +76,28 @@ describe('MRWidgetPipeline', () => { el = vm.$el; }); + afterEach(() => { + vm.$destroy(); + }); + + describe('without a pipeline', () => { + beforeEach(() => { + vm.mr = { pipeline: null }; + }); + + it('should render message with spinner', (done) => { + Vue.nextTick() + .then(() => { + expect(el.querySelector('.pipeline-id')).toBe(null); + expect(el.innerText.trim()).toBe('Waiting for pipeline...'); + expect(el.querySelectorAll('i.fa.fa-spinner.fa-spin').length).toBe(1); + done(); + }) + .then(done) + .catch(done.fail); + }); + }); + it('should render template elements correctly', () => { expect(el.classList.contains('mr-widget-heading')).toBeTruthy(); expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1); @@ -93,39 +115,47 @@ describe('MRWidgetPipeline', () => { it('should list single stage', (done) => { pipeline.details.stages.splice(0, 1); - Vue.nextTick(() => { - expect(el.querySelectorAll('.stage-container button').length).toEqual(1); - expect(el.innerText).toContain('with stage'); - done(); - }); + Vue.nextTick() + .then(() => { + expect(el.querySelectorAll('.stage-container button').length).toEqual(1); + expect(el.innerText).toContain('with stage'); + }) + .then(done) + .catch(done.fail); }); it('should not have stages when there is no stage', (done) => { vm.mr.pipeline.details.stages = []; - Vue.nextTick(() => { - expect(el.querySelectorAll('.stage-container button').length).toEqual(0); - done(); - }); + Vue.nextTick() + .then(() => { + expect(el.querySelectorAll('.stage-container button').length).toEqual(0); + }) + .then(done) + .catch(done.fail); }); it('should not have coverage text when pipeline has no coverage info', (done) => { vm.mr.pipeline.coverage = null; - Vue.nextTick(() => { - expect(el.querySelector('.js-mr-coverage')).toEqual(null); - done(); - }); + Vue.nextTick() + .then(() => { + expect(el.querySelector('.js-mr-coverage')).toEqual(null); + }) + .then(done) + .catch(done.fail); }); it('should show CI error when there is a CI error', (done) => { vm.mr.ciStatus = null; - Vue.nextTick(() => { - expect(el.querySelectorAll('.js-ci-error').length).toEqual(1); - expect(el.innerText).toContain('Could not connect to the CI server'); - done(); - }); + Vue.nextTick() + .then(() => { + expect(el.querySelectorAll('.js-ci-error').length).toEqual(1); + expect(el.innerText).toContain('Could not connect to the CI server'); + }) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 540245fe71e..1c3188cdda2 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import commitComp from '~/vue_shared/components/commit'; +import commitComp from '~/vue_shared/components/commit.vue'; describe('Commit component', () => { let props; diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index e28639f12f3..b4553acb341 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -87,7 +87,7 @@ describe('Header CI Component', () => { vm.actions[0].isLoading = true; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toEqual(''); + expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); done(); }); }); diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 4bbaff561fc..291e19c9f3c 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -4,47 +4,33 @@ import fieldComponent from '~/vue_shared/components/markdown/field.vue'; describe('Markdown field component', () => { let vm; - beforeEach(() => { + beforeEach((done) => { vm = new Vue({ - render(createElement) { - return createElement( - fieldComponent, - { - props: { - markdownPreviewUrl: '/preview', - markdownDocs: '/docs', - }, - }, - [ - createElement('textarea', { - slot: 'textarea', - }), - ], - ); + data() { + return { + text: 'testing\n123', + }; }, - }); - }); - - it('creates a new instance of GL form', (done) => { - spyOn(gl, 'GLForm'); - vm.$mount(); - - Vue.nextTick(() => { - expect( - gl.GLForm, - ).toHaveBeenCalled(); - - done(); - }); + components: { + fieldComponent, + }, + template: ` + <field-component + marodown-preview-url="/preview" + markdown-docs="/docs" + > + <textarea + slot="textarea" + v-model="text"> + </textarea> + </field-component> + `, + }).$mount(); + + Vue.nextTick(done); }); describe('mounted', () => { - beforeEach((done) => { - vm.$mount(); - - Vue.nextTick(done); - }); - it('renders textarea inside backdrop', () => { expect( vm.$el.querySelector('.zen-backdrop textarea'), @@ -117,5 +103,52 @@ describe('Markdown field component', () => { }); }); }); + + describe('markdown buttons', () => { + it('converts single words', (done) => { + const textarea = vm.$el.querySelector('textarea'); + + textarea.setSelectionRange(0, 7); + vm.$el.querySelector('.js-md').click(); + + Vue.nextTick(() => { + expect( + textarea.value, + ).toContain('**testing**'); + + done(); + }); + }); + + it('converts a line', (done) => { + const textarea = vm.$el.querySelector('textarea'); + + textarea.setSelectionRange(0, 0); + vm.$el.querySelectorAll('.js-md')[4].click(); + + Vue.nextTick(() => { + expect( + textarea.value, + ).toContain('* testing'); + + done(); + }); + }); + + it('converts multiple lines', (done) => { + const textarea = vm.$el.querySelector('textarea'); + + textarea.setSelectionRange(0, 50); + vm.$el.querySelectorAll('.js-md')[4].click(); + + Vue.nextTick(() => { + expect( + textarea.value, + ).toContain('* testing\n* 123'); + + done(); + }); + }); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js index f3b4adc0b70..b4c1f70ed1e 100644 --- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js @@ -22,7 +22,6 @@ describe('Time ago with tooltip component', () => { }).$mount(); expect(vm.$el.tagName).toEqual('TIME'); - expect(vm.$el.classList.contains('js-vue-timeago')).toEqual(true); expect( vm.$el.getAttribute('data-original-title'), ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z')); diff --git a/spec/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js new file mode 100644 index 00000000000..b1b3071527b --- /dev/null +++ b/spec/javascripts/vue_shared/directives/tooltip_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import tooltip from '~/vue_shared/directives/tooltip'; + +describe('Tooltip directive', () => { + let vm; + + afterEach(() => { + if (vm) { + vm.$destroy(); + } + }); + + describe('with a single tooltip', () => { + beforeEach(() => { + const SomeComponent = Vue.extend({ + directives: { + tooltip, + }, + template: ` + <div + v-tooltip + title="foo"> + </div> + `, + }); + + vm = new SomeComponent().$mount(); + }); + + it('should have tooltip plugin applied', () => { + expect($(vm.$el).data('bs.tooltip')).toBeDefined(); + }); + }); + + describe('with multiple tooltips', () => { + beforeEach(() => { + const SomeComponent = Vue.extend({ + directives: { + tooltip, + }, + template: ` + <div> + <div + v-tooltip + class="js-look-for-tooltip" + title="foo"> + </div> + <div + v-tooltip + title="bar"> + </div> + </div> + `, + }); + + vm = new SomeComponent().$mount(); + }); + + it('should have tooltip plugin applied to all instances', () => { + expect($(vm.$el).find('.js-look-for-tooltip').data('bs.tooltip')).toBeDefined(); + }); + }); +}); diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index deaabceef1c..787212581e2 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -24,8 +24,8 @@ describe Banzai::CrossProjectReference, lib: true do it 'returns the referenced project' do project2 = double('referenced project') - expect(Project).to receive(:find_by_full_path). - with('cross/reference').and_return(project2) + expect(Project).to receive(:find_by_full_path) + .with('cross/reference').and_return(project2) expect(project_from_ref('cross/reference')).to eq project2 end diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb index 787c2372c5b..27532f96f56 100644 --- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb @@ -23,11 +23,11 @@ describe Banzai::Filter::AbstractReferenceFilter do doc = Nokogiri::HTML.fragment('') filter = described_class.new(doc, project: project) - expect(filter).to receive(:references_per_project). - and_return({ project.path_with_namespace => Set.new(%w[1]) }) + expect(filter).to receive(:references_per_project) + .and_return({ project.path_with_namespace => Set.new(%w[1]) }) - expect(filter.projects_per_reference). - to eq({ project.path_with_namespace => project }) + expect(filter.projects_per_reference) + .to eq({ project.path_with_namespace => project }) end end @@ -37,26 +37,26 @@ describe Banzai::Filter::AbstractReferenceFilter do context 'with RequestStore disabled' do it 'returns a list of Projects for a list of paths' do - expect(filter.find_projects_for_paths([project.path_with_namespace])). - to eq([project]) + expect(filter.find_projects_for_paths([project.path_with_namespace])) + .to eq([project]) end it "return an empty array for paths that don't exist" do - expect(filter.find_projects_for_paths(['nonexistent/project'])). - to eq([]) + expect(filter.find_projects_for_paths(['nonexistent/project'])) + .to eq([]) end end context 'with RequestStore enabled', :request_store do it 'returns a list of Projects for a list of paths' do - expect(filter.find_projects_for_paths([project.path_with_namespace])). - to eq([project]) + expect(filter.find_projects_for_paths([project.path_with_namespace])) + .to eq([project]) end context "when no project with that path exists" do it "returns no value" do - expect(filter.find_projects_for_paths(['nonexistent/project'])). - to eq([]) + expect(filter.find_projects_for_paths(['nonexistent/project'])) + .to eq([]) end it "adds the ref to the project refs cache" do @@ -75,8 +75,8 @@ describe Banzai::Filter::AbstractReferenceFilter do end it "return an empty array for paths that don't exist" do - expect(filter.find_projects_for_paths(['nonexistent/project'])). - to eq([]) + expect(filter.find_projects_for_paths(['nonexistent/project'])) + .to eq([]) end end end diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index deadc36524c..fc67c7ec3c4 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -28,15 +28,15 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'links to a valid two-dot reference' do doc = reference_filter("See #{reference2}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) end it 'links to a valid three-dot reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) end it 'links to a valid short ID' do @@ -105,15 +105,15 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.css('a').first.text). - to eql("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") + expect(doc.css('a').first.text) + .to eql("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") end it 'has valid text' do @@ -140,15 +140,15 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.css('a').first.text). - to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + expect(doc.css('a').first.text) + .to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") end it 'has valid text' do @@ -175,15 +175,15 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.css('a').first.text). - to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + expect(doc.css('a').first.text) + .to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") end it 'has valid text' do @@ -214,8 +214,8 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq reference + expect(doc.css('a').first.attr('href')) + .to eq reference end it 'links with adjacent text' do diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index a19aac61229..c4d8d3b6682 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -26,8 +26,8 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do doc = reference_filter("See #{reference[0...size]}") expect(doc.css('a').first.text).to eq commit.short_id - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project.namespace, project, reference) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_commit_url(project.namespace, project, reference) end end @@ -180,8 +180,8 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) end it 'links with adjacent text' do diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index fbf7a461fa5..a4bb043f8f1 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -58,8 +58,8 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do end it 'escapes the title attribute' do - allow(project.external_issue_tracker).to receive(:title). - and_return(%{"></a>whatever<a title="}) + allow(project.external_issue_tracker).to receive(:title) + .and_return(%{"></a>whatever<a title="}) doc = filter("Issue #{reference}") expect(doc.text).to eq "Issue #{reference}" @@ -82,7 +82,9 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do context 'with RequestStore enabled' do let(:reference_filter) { HTML::Pipeline.new([described_class]) } - before { allow(RequestStore).to receive(:active?).and_return(true) } + before do + allow(RequestStore).to receive(:active?).and_return(true) + end it 'queries the collection on the first call' do expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index f1082495fcc..e5c1deb338b 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -49,8 +49,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("Fixed #{reference}") - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project) + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(issue.iid, project) end it 'links with adjacent text' do @@ -137,9 +137,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } it 'ignores valid references when cross-reference project uses external tracker' do - expect_any_instance_of(described_class).to receive(:find_object). - with(project2, issue.iid). - and_return(nil) + expect_any_instance_of(described_class).to receive(:find_object) + .with(project2, issue.iid) + .and_return(nil) exp = act = "Issue #{reference}" expect(reference_filter(act).to_html).to eq exp @@ -148,8 +148,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(issue.iid, project2) end it 'link has valid text' do @@ -181,9 +181,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } it 'ignores valid references when cross-reference project uses external tracker' do - expect_any_instance_of(described_class).to receive(:find_object). - with(project2, issue.iid). - and_return(nil) + expect_any_instance_of(described_class).to receive(:find_object) + .with(project2, issue.iid) + .and_return(nil) exp = act = "Issue #{reference}" expect(reference_filter(act).to_html).to eq exp @@ -192,8 +192,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(issue.iid, project2) end it 'link has valid text' do @@ -225,9 +225,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { "#{project2.path}##{issue.iid}" } it 'ignores valid references when cross-reference project uses external tracker' do - expect_any_instance_of(described_class).to receive(:find_object). - with(project2, issue.iid). - and_return(nil) + expect_any_instance_of(described_class).to receive(:find_object) + .with(project2, issue.iid) + .and_return(nil) exp = act = "Issue #{reference}" expect(reference_filter(act).to_html).to eq exp @@ -236,8 +236,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(issue.iid, project2) end it 'link has valid text' do @@ -270,8 +270,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq reference + expect(doc.css('a').first.attr('href')) + .to eq reference end it 'links with adjacent text' do @@ -292,8 +292,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(issue.iid, project2) end it 'links with adjacent text' do @@ -314,8 +314,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") - expect(doc.css('a').first.attr('href')). - to eq helper.url_for_issue(issue.iid, project2) + "#note_123" + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(issue.iid, project2) + "#note_123" end it 'links with adjacent text' do @@ -330,14 +330,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do doc = Nokogiri::HTML.fragment('') filter = described_class.new(doc, project: project) - expect(filter).to receive(:projects_per_reference). - and_return({ project.path_with_namespace => project }) + expect(filter).to receive(:projects_per_reference) + .and_return({ project.path_with_namespace => project }) - expect(filter).to receive(:references_per_project). - and_return({ project.path_with_namespace => Set.new([issue.iid]) }) + expect(filter).to receive(:references_per_project) + .and_return({ project.path_with_namespace => Set.new([issue.iid]) }) - expect(filter.issues_per_project). - to eq({ project => { issue.iid => issue } }) + expect(filter.issues_per_project) + .to eq({ project => { issue.iid => issue } }) end end @@ -348,14 +348,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(project).to receive(:default_issues_tracker?).and_return(false) - expect(filter).to receive(:projects_per_reference). - and_return({ project.path_with_namespace => project }) + expect(filter).to receive(:projects_per_reference) + .and_return({ project.path_with_namespace => project }) - expect(filter).to receive(:references_per_project). - and_return({ project.path_with_namespace => Set.new([1]) }) + expect(filter).to receive(:references_per_project) + .and_return({ project.path_with_namespace => Set.new([1]) }) - expect(filter.issues_per_project[project][1]). - to be_an_instance_of(ExternalIssue) + expect(filter.issues_per_project[project][1]) + .to be_an_instance_of(ExternalIssue) end end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 284641fb20a..cb3cf982351 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -72,8 +72,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) end it 'links with adjacent text' do @@ -95,8 +95,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See gfm' end @@ -119,8 +119,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See 2fa' end @@ -143,8 +143,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See ?g.fm&' end @@ -168,8 +168,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See gfm references' end @@ -192,8 +192,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See 2 factor authentication' end @@ -216,8 +216,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See g.fm & references?' end @@ -287,8 +287,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: label.name) end it 'links with adjacent text' do @@ -324,8 +324,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}", project: project) - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: group_label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: group_label.name) expect(doc.text).to eq 'See gfm references' end @@ -347,8 +347,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}", project: project) - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: group_label.name) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_issues_url(project.namespace, project, label_name: group_label.name) expect(doc.text).to eq "See gfm references" end @@ -447,8 +447,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end it 'has valid color' do - expect(result.css('a span').first.attr('style')). - to match /background-color: #00ff00/ + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ end it 'has valid link text' do @@ -483,18 +483,18 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end it 'has valid color' do - expect(result.css('a span').first.attr('style')). - to match /background-color: #00ff00/ + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ end it 'has valid link text' do - expect(result.css('a').first.text). - to eq "#{group_label.name} in #{another_project.name_with_namespace}" + expect(result.css('a').first.text) + .to eq "#{group_label.name} in #{another_project.name_with_namespace}" end it 'has valid text' do - expect(result.text). - to eq "See #{group_label.name} in #{another_project.name_with_namespace}" + expect(result.text) + .to eq "See #{group_label.name} in #{another_project.name_with_namespace}" end it 'ignores invalid IDs on the referenced label' do @@ -513,25 +513,25 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do let!(:result) { reference_filter("See #{reference}", project: project) } it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')). - to eq urls.namespace_project_issues_url(another_project.namespace, + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, another_project, label_name: group_label.name) end it 'has valid color' do - expect(result.css('a span').first.attr('style')). - to match /background-color: #00ff00/ + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ end it 'has valid link text' do - expect(result.css('a').first.text). - to eq "#{group_label.name} in #{another_project.name}" + expect(result.css('a').first.text) + .to eq "#{group_label.name} in #{another_project.name}" end it 'has valid text' do - expect(result.text). - to eq "See #{group_label.name} in #{another_project.name}" + expect(result.text) + .to eq "See #{group_label.name} in #{another_project.name}" end it 'ignores invalid IDs on the referenced label' do @@ -590,8 +590,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end it 'has valid color' do - expect(result.css('a span').first.attr('style')). - to match /background-color: #00ff00/ + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ end it 'has valid link text' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 40232f6e426..cd91681551e 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -36,8 +36,8 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_merge_request_url(project.namespace, project, merge) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_merge_request_url(project.namespace, project, merge) end it 'links with adjacent text' do @@ -107,8 +107,8 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_merge_request_url(project2.namespace, + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_merge_request_url(project2.namespace, project2, merge) end @@ -141,8 +141,8 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_merge_request_url(project2.namespace, + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_merge_request_url(project2.namespace, project2, merge) end @@ -175,8 +175,8 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_merge_request_url(project2.namespace, + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_merge_request_url(project2.namespace, project2, merge) end @@ -208,8 +208,8 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq reference + expect(doc.css('a').first.attr('href')) + .to eq reference end it 'links with adjacent text' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index a317c751d32..fe88b9cb73e 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -44,16 +44,16 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) - expect(link).to eq urls. - namespace_project_milestone_path(project.namespace, project, milestone) + expect(link).to eq urls + .namespace_project_milestone_path(project.namespace, project, milestone) end context 'Integer-based references' do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_milestone_url(project.namespace, project, milestone) end it 'links with adjacent text' do @@ -75,8 +75,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_milestone_url(project.namespace, project, milestone) expect(doc.text).to eq 'See gfm' end @@ -99,8 +99,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_milestone_url(project.namespace, project, milestone) expect(doc.text).to eq 'See gfm references' end @@ -122,8 +122,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_milestone_url(project.namespace, project, milestone) end it 'links with adjacent text' do @@ -156,8 +156,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do - expect(result.css('a').first.attr('href')).to eq urls. - namespace_project_milestone_url(another_project.namespace, + expect(result.css('a').first.attr('href')).to eq urls + .namespace_project_milestone_url(another_project.namespace, another_project, milestone) end @@ -165,15 +165,15 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'link has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.css('a').first.text). - to eq("#{milestone.name} in #{another_project.path_with_namespace}") + expect(doc.css('a').first.text) + .to eq("#{milestone.name} in #{another_project.path_with_namespace}") end it 'has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.text). - to eq("See (#{milestone.name} in #{another_project.path_with_namespace}.)") + expect(doc.text) + .to eq("See (#{milestone.name} in #{another_project.path_with_namespace}.)") end it 'escapes the name attribute' do @@ -181,8 +181,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.text). - to eq "#{milestone.name} in #{another_project.path_with_namespace}" + expect(doc.css('a').first.text) + .to eq "#{milestone.name} in #{another_project.path_with_namespace}" end end @@ -195,8 +195,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do - expect(result.css('a').first.attr('href')).to eq urls. - namespace_project_milestone_url(another_project.namespace, + expect(result.css('a').first.attr('href')).to eq urls + .namespace_project_milestone_url(another_project.namespace, another_project, milestone) end @@ -204,15 +204,15 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'link has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.css('a').first.text). - to eq("#{milestone.name} in #{another_project.path}") + expect(doc.css('a').first.text) + .to eq("#{milestone.name} in #{another_project.path}") end it 'has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.text). - to eq("See (#{milestone.name} in #{another_project.path}.)") + expect(doc.text) + .to eq("See (#{milestone.name} in #{another_project.path}.)") end it 'escapes the name attribute' do @@ -220,8 +220,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.text). - to eq "#{milestone.name} in #{another_project.path}" + expect(doc.css('a').first.text) + .to eq "#{milestone.name} in #{another_project.path}" end end @@ -234,8 +234,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do - expect(result.css('a').first.attr('href')).to eq urls. - namespace_project_milestone_url(another_project.namespace, + expect(result.css('a').first.attr('href')).to eq urls + .namespace_project_milestone_url(another_project.namespace, another_project, milestone) end @@ -243,15 +243,15 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'link has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.css('a').first.text). - to eq("#{milestone.name} in #{another_project.path}") + expect(doc.css('a').first.text) + .to eq("#{milestone.name} in #{another_project.path}") end it 'has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.text). - to eq("See (#{milestone.name} in #{another_project.path}.)") + expect(doc.text) + .to eq("See (#{milestone.name} in #{another_project.path}.)") end it 'escapes the name attribute' do @@ -259,8 +259,8 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.text). - to eq "#{milestone.name} in #{another_project.path}" + expect(doc.css('a').first.text) + .to eq "#{milestone.name} in #{another_project.path}" end end end diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index 7c4a0f32c7b..b81cdbb8957 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -33,13 +33,15 @@ describe Banzai::Filter::RedactorFilter, lib: true do end before do - allow(Banzai::ReferenceParser).to receive(:[]). - with('test'). - and_return(parser_class) + allow(Banzai::ReferenceParser).to receive(:[]) + .with('test') + .and_return(parser_class) end context 'valid projects' do - before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) } + before do + allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) + end it 'allows permitted Project references' do user = create(:user) @@ -54,7 +56,9 @@ describe Banzai::Filter::RedactorFilter, lib: true do end context 'invalid projects' do - before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) } + before do + allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) + end it 'removes unpermitted references' do user = create(:user) diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/reference_filter_spec.rb index 55e681f6faf..ba0fa4a609a 100644 --- a/spec/lib/banzai/filter/reference_filter_spec.rb +++ b/spec/lib/banzai/filter/reference_filter_spec.rb @@ -8,8 +8,8 @@ describe Banzai::Filter::ReferenceFilter, lib: true do document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') filter = described_class.new(document, project: project) - expect { |b| filter.each_node(&b) }. - to yield_with_args(an_instance_of(Nokogiri::XML::Element)) + expect { |b| filter.each_node(&b) } + .to yield_with_args(an_instance_of(Nokogiri::XML::Element)) end it 'returns an Enumerator when no block is given' do diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 1957ba739e2..1ce7bd7706e 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -72,15 +72,15 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do it 'ignores ref if commit is passed' do doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') ) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/#{ref}/non/existent.file" # non-existent files have no leading blob/raw/tree + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/#{ref}/non/existent.file" # non-existent files have no leading blob/raw/tree end shared_examples :valid_repository do it 'rebuilds absolute URL for a file in the repo' do doc = filter(link('/doc/api/README.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end it 'ignores absolute URLs with two leading slashes' do @@ -90,71 +90,71 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do it 'rebuilds relative URL for a file in the repo' do doc = filter(link('doc/api/README.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end it 'rebuilds relative URL for a file in the repo with leading ./' do doc = filter(link('./doc/api/README.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end it 'rebuilds relative URL for a file in the repo up one directory' do relative_link = link('../api/README.md') doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end it 'rebuilds relative URL for a file in the repo up multiple directories' do relative_link = link('../../../api/README.md') doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md') - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end it 'rebuilds relative URL for a file in the repository root' do relative_link = link('../README.md') doc = filter(relative_link, requested_path: 'doc/some-file.md') - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/README.md" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/blob/#{ref}/README.md" end it 'rebuilds relative URL for a file in the repo with an anchor' do doc = filter(link('README.md#section')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/README.md#section" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/blob/#{ref}/README.md#section" end it 'rebuilds relative URL for a directory in the repo' do doc = filter(link('doc/api/')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/tree/#{ref}/doc/api" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/tree/#{ref}/doc/api" end it 'rebuilds relative URL for an image in the repo' do doc = filter(image('files/images/logo-black.png')) - expect(doc.at_css('img')['src']). - to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + expect(doc.at_css('img')['src']) + .to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" end it 'rebuilds relative URL for link to an image in the repo' do doc = filter(link('files/images/logo-black.png')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + expect(doc.at_css('a')['href']) + .to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" end it 'rebuilds relative URL for a video in the repo' do doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video') - expect(doc.at_css('video')['src']). - to eq "/#{project_path}/raw/video/files/videos/intro.mp4" + expect(doc.at_css('video')['src']) + .to eq "/#{project_path}/raw/video/files/videos/intro.mp4" end it 'does not modify relative URL with an anchor only' do diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index fb7862f49a2..a8a0aa6d395 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -221,8 +221,8 @@ describe Banzai::Filter::SanitizationFilter, lib: true do end it 'disallows invalid URIs' do - expect(Addressable::URI).to receive(:parse).with('foo://example.com'). - and_raise(Addressable::URI::InvalidURIError) + expect(Addressable::URI).to receive(:parse).with('foo://example.com') + .and_raise(Addressable::URI::InvalidURIError) input = '<a href="foo://example.com">Foo</a>' output = filter(input) diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index e036514d283..e851120bc3a 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -22,8 +22,8 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_snippet_url(project.namespace, project, snippet) + expect(doc.css('a').first.attr('href')).to eq urls + .namespace_project_snippet_url(project.namespace, project, snippet) end it 'links with adjacent text' do @@ -88,8 +88,8 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end it 'link has valid text' do @@ -121,8 +121,8 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end it 'link has valid text' do @@ -154,8 +154,8 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end it 'link has valid text' do @@ -186,8 +186,8 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'links to a valid reference' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end it 'links with adjacent text' do diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index 639cac6406a..6327ca8bbfd 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -51,22 +51,22 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do context 'with a valid repository' do it 'rebuilds relative URL for a link' do doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + expect(doc.at_css('a')['href']) + .to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" doc = filter(nested_link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + expect(doc.at_css('a')['href']) + .to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" end it 'rebuilds relative URL for an image' do doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('img')['src']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + expect(doc.at_css('img')['src']) + .to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" doc = filter(nested_image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('img')['src']). - to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + expect(doc.at_css('img')['src']) + .to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" end it 'does not modify absolute URL' do @@ -79,10 +79,10 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do escaped = Addressable::URI.escape(path) # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class). - to receive(:file_exists?).and_return(true) - allow_any_instance_of(described_class). - to receive(:image?).with(path).and_return(true) + allow_any_instance_of(described_class) + .to receive(:file_exists?).and_return(true) + allow_any_instance_of(described_class) + .to receive(:image?).with(path).and_return(true) doc = filter(image(escaped)) expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb index 49556074278..32764bee5eb 100644 --- a/spec/lib/banzai/note_renderer_spec.rb +++ b/spec/lib/banzai/note_renderer_spec.rb @@ -8,15 +8,15 @@ describe Banzai::NoteRenderer do wiki = double(:wiki) user = double(:user) - expect(Banzai::ObjectRenderer).to receive(:new). - with(project, user, + expect(Banzai::ObjectRenderer).to receive(:new) + .with(project, user, requested_path: 'foo', project_wiki: wiki, - ref: 'bar'). - and_call_original + ref: 'bar') + .and_call_original - expect_any_instance_of(Banzai::ObjectRenderer). - to receive(:render).with([note], :note) + expect_any_instance_of(Banzai::ObjectRenderer) + .to receive(:render).with([note], :note) described_class.render([note], project, user, 'foo', wiki, 'bar') end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index e6f2963193c..81ae5685b10 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -12,11 +12,11 @@ describe Banzai::Redactor do end it 'redacts an array of documents' do - doc1 = Nokogiri::HTML. - fragment('<a class="gfm" data-reference-type="issue">foo</a>') + doc1 = Nokogiri::HTML + .fragment('<a class="gfm" data-reference-type="issue">foo</a>') - doc2 = Nokogiri::HTML. - fragment('<a class="gfm" data-reference-type="issue">bar</a>') + doc2 = Nokogiri::HTML + .fragment('<a class="gfm" data-reference-type="issue">bar</a>') redacted_data = redactor.redact([doc1, doc2]) @@ -93,9 +93,9 @@ describe Banzai::Redactor do doc = Nokogiri::HTML.fragment('<a href="foo">foo</a>') node = doc.children[0] - expect(redactor).to receive(:nodes_visible_to_user). - with([node]). - and_return(Set.new) + expect(redactor).to receive(:nodes_visible_to_user) + .with([node]) + .and_return(Set.new) redactor.redact_document_nodes([{ document: doc, nodes: [node] }]) @@ -108,10 +108,10 @@ describe Banzai::Redactor do doc = Nokogiri::HTML.fragment('<a data-reference-type="issue"></a>') node = doc.children[0] - expect_any_instance_of(Banzai::ReferenceParser::IssueParser). - to receive(:nodes_visible_to_user). - with(user, [node]). - and_return([node]) + expect_any_instance_of(Banzai::ReferenceParser::IssueParser) + .to receive(:nodes_visible_to_user) + .with(user, [node]) + .and_return([node]) expect(redactor.nodes_visible_to_user([node])).to eq(Set.new([node])) end diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index f4f42bfc3ed..b444ca05b8e 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -54,8 +54,8 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do describe '#referenced_by' do context 'when references_relation is implemented' do it 'returns a collection of objects' do - links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>"). - children + links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>") + .children expect(subject).to receive(:references_relation).and_return(User) expect(subject.referenced_by(links)).to eq([user]) @@ -66,8 +66,8 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'raises NotImplementedError' do links = Nokogiri::HTML.fragment('<a data-foo="1"></a>').children - expect { subject.referenced_by(links) }. - to raise_error(NotImplementedError) + expect { subject.referenced_by(links) } + .to raise_error(NotImplementedError) end end end @@ -80,8 +80,8 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do describe '#gather_attributes_per_project' do it 'returns a Hash containing attribute values per project' do - link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>'). - children[0] + link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>') + .children[0] hash = subject.gather_attributes_per_project([link], 'data-foo') @@ -95,42 +95,42 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'returns a Hash grouping objects per node' do link = double(:link) - expect(link).to receive(:has_attribute?). - with('data-user'). - and_return(true) + expect(link).to receive(:has_attribute?) + .with('data-user') + .and_return(true) - expect(link).to receive(:attr). - with('data-user'). - and_return(user.id.to_s) + expect(link).to receive(:attr) + .with('data-user') + .and_return(user.id.to_s) nodes = [link] - expect(subject).to receive(:unique_attribute_values). - with(nodes, 'data-user'). - and_return([user.id.to_s]) + expect(subject).to receive(:unique_attribute_values) + .with(nodes, 'data-user') + .and_return([user.id.to_s]) hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user') expect(hash).to eq({ link => user }) end - it 'returns an empty Hash when entry does not exist in the database' do + it 'returns an empty Hash when entry does not exist in the database', :request_store do link = double(:link) - expect(link).to receive(:has_attribute?). - with('data-user'). - and_return(true) + expect(link).to receive(:has_attribute?) + .with('data-user') + .and_return(true) - expect(link).to receive(:attr). - with('data-user'). - and_return('1') + expect(link).to receive(:attr) + .with('data-user') + .and_return('1') nodes = [link] bad_id = user.id + 100 - expect(subject).to receive(:unique_attribute_values). - with(nodes, 'data-user'). - and_return([bad_id.to_s]) + expect(subject).to receive(:unique_attribute_values) + .with(nodes, 'data-user') + .and_return([bad_id.to_s]) hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user') @@ -142,15 +142,15 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'returns an Array of unique values' do link = double(:link) - expect(link).to receive(:has_attribute?). - with('data-foo'). - twice. - and_return(true) + expect(link).to receive(:has_attribute?) + .with('data-foo') + .twice + .and_return(true) - expect(link).to receive(:attr). - with('data-foo'). - twice. - and_return('1') + expect(link).to receive(:attr) + .with('data-foo') + .twice + .and_return('1') nodes = [link, link] @@ -167,9 +167,9 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do instance = dummy.new(project, user) document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>') - expect(instance).to receive(:gather_references). - with([document.children[1]]). - and_return([user]) + expect(instance).to receive(:gather_references) + .with([document.children[1]]) + .and_return([user]) expect(instance.process([document])).to eq([user]) end @@ -179,9 +179,9 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do let(:link) { double(:link) } it 'does not process links a user can not reference' do - expect(subject).to receive(:nodes_user_can_reference). - with(user, [link]). - and_return([]) + expect(subject).to receive(:nodes_user_can_reference) + .with(user, [link]) + .and_return([]) expect(subject).to receive(:referenced_by).with([]) @@ -189,13 +189,13 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do end it 'does not process links a user can not see' do - expect(subject).to receive(:nodes_user_can_reference). - with(user, [link]). - and_return([link]) + expect(subject).to receive(:nodes_user_can_reference) + .with(user, [link]) + .and_return([link]) - expect(subject).to receive(:nodes_visible_to_user). - with(user, [link]). - and_return([]) + expect(subject).to receive(:nodes_visible_to_user) + .with(user, [link]) + .and_return([]) expect(subject).to receive(:referenced_by).with([]) @@ -203,13 +203,13 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do end it 'returns the references if a user can reference and see a link' do - expect(subject).to receive(:nodes_user_can_reference). - with(user, [link]). - and_return([link]) + expect(subject).to receive(:nodes_user_can_reference) + .with(user, [link]) + .and_return([link]) - expect(subject).to receive(:nodes_visible_to_user). - with(user, [link]). - and_return([link]) + expect(subject).to receive(:nodes_visible_to_user) + .with(user, [link]) + .and_return([link]) expect(subject).to receive(:referenced_by).with([link]) @@ -221,8 +221,8 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'delegates the permissions check to the Ability class' do user = double(:user) - expect(Ability).to receive(:allowed?). - with(user, :read_project, project) + expect(Ability).to receive(:allowed?) + .with(user, :read_project, project) subject.can?(user, :read_project, project) end @@ -230,8 +230,8 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do describe '#find_projects_for_hash_keys' do it 'returns a list of Projects' do - expect(subject.find_projects_for_hash_keys(project.id => project)). - to eq([project]) + expect(subject.find_projects_for_hash_keys(project.id => project)) + .to eq([project]) end end @@ -243,8 +243,8 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do expect(collection).to receive(:where).twice.and_call_original 2.times do - expect(subject.collection_objects_for_ids(collection, [user.id])). - to eq([user]) + expect(subject.collection_objects_for_ids(collection, [user.id])) + .to eq([user]) end end end @@ -258,8 +258,8 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do end it 'queries the collection on the first call' do - expect(subject.collection_objects_for_ids(User, [user.id])). - to eq([user]) + expect(subject.collection_objects_for_ids(User, [user.id])) + .to eq([user]) end it 'does not query previously queried objects' do @@ -268,34 +268,34 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do expect(collection).to receive(:where).once.and_call_original 2.times do - expect(subject.collection_objects_for_ids(collection, [user.id])). - to eq([user]) + expect(subject.collection_objects_for_ids(collection, [user.id])) + .to eq([user]) end end it 'casts String based IDs to Fixnums before querying objects' do 2.times do - expect(subject.collection_objects_for_ids(User, [user.id.to_s])). - to eq([user]) + expect(subject.collection_objects_for_ids(User, [user.id.to_s])) + .to eq([user]) end end it 'queries any additional objects after the first call' do other_user = create(:user) - expect(subject.collection_objects_for_ids(User, [user.id])). - to eq([user]) + expect(subject.collection_objects_for_ids(User, [user.id])) + .to eq([user]) - expect(subject.collection_objects_for_ids(User, [user.id, other_user.id])). - to eq([user, other_user]) + expect(subject.collection_objects_for_ids(User, [user.id, other_user.id])) + .to eq([user, other_user]) end it 'caches objects on a per collection class basis' do - expect(subject.collection_objects_for_ids(User, [user.id])). - to eq([user]) + expect(subject.collection_objects_for_ids(User, [user.id])) + .to eq([user]) - expect(subject.collection_objects_for_ids(Project, [project.id])). - to eq([project]) + expect(subject.collection_objects_for_ids(Project, [project.id])) + .to eq([project]) end end end diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb index 412ffa77c36..a314a6119cb 100644 --- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -10,7 +10,9 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-commit'] = 123 } + before do + link['data-commit'] = 123 + end it_behaves_like "referenced feature visibility", "repository" end @@ -30,30 +32,30 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do it 'returns an Array of commits' do commit = double(:commit) - allow_any_instance_of(Project).to receive(:valid_repo?). - and_return(true) + allow_any_instance_of(Project).to receive(:valid_repo?) + .and_return(true) - expect(subject).to receive(:find_commits). - with(project, ['123']). - and_return([commit]) + expect(subject).to receive(:find_commits) + .with(project, ['123']) + .and_return([commit]) expect(subject.referenced_by([link])).to eq([commit]) end it 'returns an empty Array when the commit could not be found' do - allow_any_instance_of(Project).to receive(:valid_repo?). - and_return(true) + allow_any_instance_of(Project).to receive(:valid_repo?) + .and_return(true) - expect(subject).to receive(:find_commits). - with(project, ['123']). - and_return([]) + expect(subject).to receive(:find_commits) + .with(project, ['123']) + .and_return([]) expect(subject.referenced_by([link])).to eq([]) end it 'skips projects without valid repositories' do - allow_any_instance_of(Project).to receive(:valid_repo?). - and_return(false) + allow_any_instance_of(Project).to receive(:valid_repo?) + .and_return(false) expect(subject.referenced_by([link])).to eq([]) end @@ -61,8 +63,8 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do context 'when the link does not have a data-commit attribute' do it 'returns an empty Array' do - allow_any_instance_of(Project).to receive(:valid_repo?). - and_return(true) + allow_any_instance_of(Project).to receive(:valid_repo?) + .and_return(true) expect(subject.referenced_by([link])).to eq([]) end @@ -71,8 +73,8 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do context 'when the link does not have a data-project attribute' do it 'returns an empty Array' do - allow_any_instance_of(Project).to receive(:valid_repo?). - and_return(true) + allow_any_instance_of(Project).to receive(:valid_repo?) + .and_return(true) expect(subject.referenced_by([link])).to eq([]) end diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb index 96e55b0997a..5dca5e784da 100644 --- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -10,7 +10,9 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-commit-range'] = '123..456' } + before do + link['data-commit-range'] = '123..456' + end it_behaves_like "referenced feature visibility", "repository" end @@ -30,17 +32,17 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do it 'returns an Array of commit ranges' do range = double(:range) - expect(subject).to receive(:find_object). - with(project, '123..456'). - and_return(range) + expect(subject).to receive(:find_object) + .with(project, '123..456') + .and_return(range) expect(subject.referenced_by([link])).to eq([range]) end it 'returns an empty Array when the commit range could not be found' do - expect(subject).to receive(:find_object). - with(project, '123..456'). - and_return(nil) + expect(subject).to receive(:find_object) + .with(project, '123..456') + .and_return(nil) expect(subject.referenced_by([link])).to eq([]) end @@ -86,17 +88,17 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do it 'returns an Array of range objects' do range = double(:commit) - expect(subject).to receive(:find_object). - with(project, '123..456'). - and_return(range) + expect(subject).to receive(:find_object) + .with(project, '123..456') + .and_return(range) expect(subject.find_ranges(project, ['123..456'])).to eq([range]) end it 'skips ranges that could not be found' do - expect(subject).to receive(:find_object). - with(project, '123..456'). - and_return(nil) + expect(subject).to receive(:find_object) + .with(project, '123..456') + .and_return(nil) expect(subject.find_ranges(project, ['123..456'])).to eq([]) end diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb index 0af36776a54..d212bbac619 100644 --- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -10,7 +10,9 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-external-issue'] = 123 } + before do + link['data-external-issue'] = 123 + end levels = [ProjectFeature::DISABLED, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 7031c47231c..58e1a0c1bc1 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -18,17 +18,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do it_behaves_like "referenced feature visibility", "issues" it 'returns the nodes when the user can read the issue' do - expect(Ability).to receive(:issues_readable_by_user). - with([issue], user). - and_return([issue]) + expect(Ability).to receive(:issues_readable_by_user) + .with([issue], user) + .and_return([issue]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end it 'returns an empty Array when the user can not read the issue' do - expect(Ability).to receive(:issues_readable_by_user). - with([issue], user). - and_return([]) + expect(Ability).to receive(:issues_readable_by_user) + .with([issue], user) + .and_return([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb index 8c540d35ddd..ddd699f3c25 100644 --- a/spec/lib/banzai/reference_parser/label_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb @@ -11,7 +11,9 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-label'] = label.id.to_s } + before do + link['data-label'] = label.id.to_s + end it_behaves_like "referenced feature visibility", "issues", "merge_requests" end diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb index 2d4d589ae34..72d4f3bc18e 100644 --- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb @@ -11,7 +11,9 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-milestone'] = milestone.id.to_s } + before do + link['data-milestone'] = milestone.id.to_s + end it_behaves_like "referenced feature visibility", "issues", "merge_requests" end diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 4d560667342..dfebb971f3a 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -96,17 +96,17 @@ describe Banzai::ReferenceParser::UserParser, lib: true do end it 'returns the nodes if the user can read the group' do - expect(Ability).to receive(:allowed?). - with(user, :read_group, group). - and_return(true) + expect(Ability).to receive(:allowed?) + .with(user, :read_group, group) + .and_return(true) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end it 'returns an empty Array if the user can not read the group' do - expect(Ability).to receive(:allowed?). - with(user, :read_group, group). - and_return(false) + expect(Ability).to receive(:allowed?) + .with(user, :read_group, group) + .and_return(false) expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end @@ -129,9 +129,9 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability).to receive(:allowed?). - with(user, :read_project, other_project). - and_return(true) + expect(Ability).to receive(:allowed?) + .with(user, :read_project, other_project) + .and_return(true) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end @@ -141,9 +141,9 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability).to receive(:allowed?). - with(user, :read_project, other_project). - and_return(false) + expect(Ability).to receive(:allowed?) + .with(user, :read_project, other_project) + .and_return(false) expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index fb6cc398307..51cbfd2a848 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -1,21 +1,21 @@ require 'spec_helper' describe Ci::Charts, lib: true do - context "build_times" do + context "pipeline_times" do let(:project) { create(:empty_project) } - let(:chart) { Ci::Charts::BuildTime.new(project) } + let(:chart) { Ci::Charts::PipelineTime.new(project) } - subject { chart.build_times } + subject { chart.pipeline_times } before do create(:ci_empty_pipeline, project: project, duration: 120) end - it 'returns build times in minutes' do + it 'returns pipeline times in minutes' do is_expected.to contain_exactly(2) end - it 'handles nil build times' do + it 'handles nil pipeline times' do create(:ci_empty_pipeline, project: project, duration: nil) is_expected.to contain_exactly(2, 0) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 2ca0773ad1d..af0e7855a9b 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -596,62 +596,117 @@ module Ci end describe "Image and service handling" do - it "returns image and service when defined" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { script: "rspec" } - }) + context "when extended docker configuration is used" do + it "returns image and service when defined" do + config = YAML.dump({ image: { name: "ruby:2.1" }, + services: ["mysql", { name: "docker:dind", alias: "docker" }], + before_script: ["pwd"], + rspec: { script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config, path) + config_processor = GitlabCiYamlProcessor.new(config, path) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - image: "ruby:2.1", - services: ["mysql"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: { name: "ruby:2.5" }, + services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.5" }, + services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end end - it "returns image and service when overridden for job" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: "ruby:2.5", services: ["postgresql"], script: "rspec" } - }) + context "when etended docker configuration is not used" do + it "returns image and service when defined" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql", "docker:dind"], + before_script: ["pwd"], + rspec: { script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config, path) + config_processor = GitlabCiYamlProcessor.new(config, path) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - image: "ruby:2.5", - services: ["postgresql"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.5" }, + services: [{ name: "postgresql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end end end @@ -884,8 +939,8 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: "ruby:2.1", - services: ["mysql"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }], artifacts: { name: "custom_name", paths: ["logs/", "binaries/"], @@ -1261,7 +1316,7 @@ EOT config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string") end it "returns errors if job name is blank" do @@ -1282,35 +1337,35 @@ EOT config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") end it "returns errors if services parameter is not an array" do config = YAML.dump({ services: "test", rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array") end it "returns errors if services parameter is not an array of strings" do config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") end it "returns errors if job services parameter is not an array" do config = YAML.dump({ rspec: { script: "test", services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array") end it "returns errors if job services parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") end it "returns error if job configuration is invalid" do @@ -1324,7 +1379,7 @@ EOT config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array") end it "returns errors if there are no jobs defined" do diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index ab010c6dfeb..175fd2e7e13 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -72,8 +72,8 @@ describe ContainerRegistry::Blob do describe '#data' do context 'when locally stored' do before do - stub_request(:get, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345'). - to_return( + stub_request(:get, 'http://registry.gitlab/v2/group/test/image/blobs/sha256:0123456789012345') + .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, body: '{"key":"value"}') @@ -97,9 +97,9 @@ describe ContainerRegistry::Blob do context 'for a valid address' do before do - stub_request(:get, location). - with { |request| !request.headers.include?('Authorization') }. - to_return( + stub_request(:get, location) + .with { |request| !request.headers.include?('Authorization') } + .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, body: '{"key":"value"}') diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb index ec03b533383..3df33f48adb 100644 --- a/spec/lib/container_registry/client_spec.rb +++ b/spec/lib/container_registry/client_spec.rb @@ -8,28 +8,28 @@ describe ContainerRegistry::Client do describe '#blob' do it 'GET /v2/:name/blobs/:digest' do - stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345"). - with(headers: { + stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345") + .with(headers: { 'Accept' => 'application/octet-stream', 'Authorization' => "bearer #{token}" - }). - to_return(status: 200, body: "Blob") + }) + .to_return(status: 200, body: "Blob") expect(client.blob('group/test', 'sha256:0123456789012345')).to eq('Blob') end it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do - stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345"). - with(headers: { + stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345") + .with(headers: { 'Accept' => 'application/octet-stream', 'Authorization' => "bearer #{token}" - }). - to_return(status: 307, body: "", headers: { Location: 'http://redirected' }) + }) + .to_return(status: 307, body: "", headers: { Location: 'http://redirected' }) # We should probably use hash_excluding here, but that requires an update to WebMock: # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb - stub_request(:get, "http://redirected/"). - with { |request| !request.headers.include?('Authorization') }. - to_return(status: 200, body: "Successfully redirected") + stub_request(:get, "http://redirected/") + .with { |request| !request.headers.include?('Authorization') } + .to_return(status: 200, body: "Successfully redirected") response = client.blob('group/test', 'sha256:0123456789012345') diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index f8fffbdca41..cb4ae3be525 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -60,9 +60,9 @@ describe ContainerRegistry::Tag do context 'manifest processing' do context 'schema v1' do before do - stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag'). - with(headers: headers). - to_return( + stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag') + .with(headers: headers) + .to_return( status: 200, body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest_1.json'), headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v1+prettyjws' }) @@ -97,9 +97,9 @@ describe ContainerRegistry::Tag do context 'schema v2' do before do - stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag'). - with(headers: headers). - to_return( + stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag') + .with(headers: headers) + .to_return( status: 200, body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'), headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' }) @@ -134,9 +134,9 @@ describe ContainerRegistry::Tag do context 'when locally stored' do before do - stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). - with(headers: { 'Accept' => 'application/octet-stream' }). - to_return( + stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac') + .with(headers: { 'Accept' => 'application/octet-stream' }) + .to_return( status: 200, body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) end @@ -146,14 +146,14 @@ describe ContainerRegistry::Tag do context 'when externally stored' do before do - stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). - with(headers: { 'Accept' => 'application/octet-stream' }). - to_return( + stub_request(:get, 'http://registry.gitlab/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac') + .with(headers: { 'Accept' => 'application/octet-stream' }) + .to_return( status: 307, headers: { 'Location' => 'http://external.com/blob/file' }) - stub_request(:get, 'http://external.com/blob/file'). - to_return( + stub_request(:get, 'http://external.com/blob/file') + .to_return( status: 200, body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 33ab005667a..f2132d485ab 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -14,8 +14,8 @@ describe ExtractsPath, lib: true do repo = double(ref_names: ['master', 'foo/bar/baz', 'v1.0.0', 'v2.0.0', 'release/app', 'release/app/v1.0.0']) allow(project).to receive(:repository).and_return(repo) - allow(project).to receive(:path_with_namespace). - and_return('gitlab/gitlab-ci') + allow(project).to receive(:path_with_namespace) + .and_return('gitlab/gitlab-ci') allow(request).to receive(:format=) end @@ -77,7 +77,10 @@ describe ExtractsPath, lib: true do context 'without a path' do let(:params) { { ref: 'v1.0.0.atom' } } - before { assign_ref_vars } + + before do + assign_ref_vars + end it 'sets the un-suffixed version as @ref' do expect(@ref).to eq('v1.0.0') diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 1d92a5cb33f..5cc3a3745e4 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -6,8 +6,8 @@ describe Feature, lib: true do let(:key) { 'my_feature' } it 'returns the Flipper feature' do - expect_any_instance_of(Flipper::DSL).to receive(:feature).with(key). - and_return(feature) + expect_any_instance_of(Flipper::DSL).to receive(:feature).with(key) + .and_return(feature) expect(described_class.get(key)).to be(feature) end @@ -17,8 +17,8 @@ describe Feature, lib: true do let(:features) { Set.new } it 'returns the Flipper features as an array' do - expect_any_instance_of(Flipper::DSL).to receive(:features). - and_return(features) + expect_any_instance_of(Flipper::DSL).to receive(:features) + .and_return(features) expect(described_class.all).to eq(features.to_a) end diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index 94dcddcc30c..fc72df575be 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -40,7 +40,9 @@ describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do end context 'allow 2 unique ips' do - before { current_application_settings.update!(unique_ips_limit_per_user: 2) } + before do + current_application_settings.update!(unique_ips_limit_per_user: 2) + end it 'blocks user trying to login from third ip' do change_ip('ip1') diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb index f2073b9bcb3..64f82fe27b2 100644 --- a/spec/lib/gitlab/background_migration_spec.rb +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -5,9 +5,9 @@ describe Gitlab::BackgroundMigration do it 'steals jobs from a queue' do queue = [double(:job, args: ['Foo', [10, 20]])] - allow(Sidekiq::Queue).to receive(:new). - with(BackgroundMigrationWorker.sidekiq_options['queue']). - and_return(queue) + allow(Sidekiq::Queue).to receive(:new) + .with(BackgroundMigrationWorker.sidekiq_options['queue']) + .and_return(queue) expect(queue[0]).to receive(:delete) @@ -19,9 +19,9 @@ describe Gitlab::BackgroundMigration do it 'does not steal jobs for a different migration' do queue = [double(:job, args: ['Foo', [10, 20]])] - allow(Sidekiq::Queue).to receive(:new). - with(BackgroundMigrationWorker.sidekiq_options['queue']). - and_return(queue) + allow(Sidekiq::Queue).to receive(:new) + .with(BackgroundMigrationWorker.sidekiq_options['queue']) + .and_return(queue) expect(described_class).not_to receive(:perform) @@ -36,9 +36,9 @@ describe Gitlab::BackgroundMigration do instance = double(:instance) klass = double(:klass, new: instance) - expect(described_class).to receive(:const_get). - with('Foo'). - and_return(klass) + expect(described_class).to receive(:const_get) + .with('Foo') + .and_return(klass) expect(instance).to receive(:perform).with(10, 20) diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 3c5414701a7..6abf4ca46a9 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -29,7 +29,9 @@ describe Gitlab::Badge::Build::Status do let!(:build) { create_build(project, sha, branch) } context 'build success' do - before { build.success! } + before do + build.success! + end describe '#status' do it 'is successful' do @@ -39,7 +41,9 @@ describe Gitlab::Badge::Build::Status do end context 'build failed' do - before { build.drop! } + before do + build.drop! + end describe '#status' do it 'failed' do diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index a7ee7f53a6b..d8beb05601c 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -86,11 +86,9 @@ describe Gitlab::BitbucketImport::Importer, lib: true do headers: { "Content-Type" => "application/json" }, body: issues_statuses_sample_data.to_json) - stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on"). - with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }). - to_return(status: 200, - body: "", - headers: {}) + stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on") + .with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }) + .to_return(status: 200, body: "", headers: {}) sample_issues_statuses.each_with_index do |issue, index| stub_request( diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index cfb5cba054e..07db6c3a640 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -37,11 +37,11 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do loaded_from_cache: false ) - expect(described_class).to receive(:new). - with(project_without_status, + expect(described_class).to receive(:new) + .with(project_without_status, pipeline_info: empty_status, - loaded_from_cache: false). - and_return(fake_pipeline) + loaded_from_cache: false) + .and_return(fake_pipeline) expect(fake_pipeline).to receive(:load_from_project) expect(fake_pipeline).to receive(:store_in_cache) @@ -112,12 +112,12 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do pipeline = build_stubbed(:ci_pipeline, sha: '123456', status: 'success', ref: 'master') fake_status = double - expect(described_class).to receive(:new). - with(pipeline.project, + expect(described_class).to receive(:new) + .with(pipeline.project, pipeline_info: { sha: '123456', status: 'success', ref: 'master' - }). - and_return(fake_status) + }) + .and_return(fake_status) expect(fake_status).to receive(:store_in_cache_if_needed) diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index c0c309d8179..643e590438a 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -20,7 +20,9 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ).exec end - before { project.add_developer(user) } + before do + project.add_developer(user) + end context 'without failed checks' do it "doesn't raise an error" do @@ -50,7 +52,9 @@ describe Gitlab::Checks::ChangeAccess, lib: true do let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } context 'as master' do - before { project.add_master(user) } + before do + project.add_master(user) + end context 'deletion' do let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb index eea01f91879..6a52ae01b2f 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -33,8 +33,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do subject { metadata('other_artifacts_0.1.2/').find_entries! } it 'matches correct paths' do - expect(subject.keys). - to contain_exactly 'other_artifacts_0.1.2/', + expect(subject.keys) + .to contain_exactly 'other_artifacts_0.1.2/', 'other_artifacts_0.1.2/doc_sample.txt', 'other_artifacts_0.1.2/another-subdirectory/' end @@ -44,8 +44,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! } it 'matches correct paths' do - expect(subject.keys). - to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/', + expect(subject.keys) + .to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/', 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' end @@ -55,8 +55,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do subject { metadata('other_artifacts_0.1.2/', recursive: true).find_entries! } it 'matches correct paths' do - expect(subject.keys). - to contain_exactly 'other_artifacts_0.1.2/', + expect(subject.keys) + .to contain_exactly 'other_artifacts_0.1.2/', 'other_artifacts_0.1.2/doc_sample.txt', 'other_artifacts_0.1.2/another-subdirectory/', 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 382385dfd6b..773a52cdfbc 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -10,12 +10,28 @@ describe Gitlab::Ci::Build::Image do let(:image_name) { 'ruby:2.1' } let(:job) { create(:ci_build, options: { image: image_name } ) } - it 'fabricates an object of the proper class' do - is_expected.to be_kind_of(described_class) + context 'when image is defined as string' do + it 'fabricates an object of the proper class' do + is_expected.to be_kind_of(described_class) + end + + it 'populates fabricated object with the proper name attribute' do + expect(subject.name).to eq(image_name) + end end - it 'populates fabricated object with the proper name attribute' do - expect(subject.name).to eq(image_name) + context 'when image is defined as hash' do + let(:entrypoint) { '/bin/sh' } + let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) } + + it 'fabricates an object of the proper class' do + is_expected.to be_kind_of(described_class) + end + + it 'populates fabricated object with the proper attributes' do + expect(subject.name).to eq(image_name) + expect(subject.entrypoint).to eq(entrypoint) + end end context 'when image name is empty' do @@ -41,10 +57,39 @@ describe Gitlab::Ci::Build::Image do let(:service_image_name) { 'postgres' } let(:job) { create(:ci_build, options: { services: [service_image_name] }) } - it 'fabricates an non-empty array of objects' do - is_expected.to be_kind_of(Array) - is_expected.not_to be_empty - expect(subject.first.name).to eq(service_image_name) + context 'when service is defined as string' do + it 'fabricates an non-empty array of objects' do + is_expected.to be_kind_of(Array) + is_expected.not_to be_empty + end + + it 'populates fabricated objects with the proper name attributes' do + expect(subject.first).to be_kind_of(described_class) + expect(subject.first.name).to eq(service_image_name) + end + end + + context 'when service is defined as hash' do + let(:service_entrypoint) { '/bin/sh' } + let(:service_alias) { 'db' } + let(:service_command) { 'sleep 30' } + let(:job) do + create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, + alias: service_alias, command: service_command }] }) + end + + it 'fabricates an non-empty array of objects' do + is_expected.to be_kind_of(Array) + is_expected.not_to be_empty + expect(subject.first).to be_kind_of(described_class) + end + + it 'populates fabricated objects with the proper attributes' do + expect(subject.first.name).to eq(service_image_name) + expect(subject.first.entrypoint).to eq(service_entrypoint) + expect(subject.first.alias).to eq(service_alias) + expect(subject.first.command).to eq(service_command) + end end context 'when service image name is empty' do diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 2ed120f356a..878b1d6b862 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::Ci::Config::Entry::Cache do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry config value is correct' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index c330e609337..3c0007f4d57 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Environment do let(:entry) { described_class.new(config) } - before { entry.compose! } + before do + entry.compose! + end context 'when configuration is a string' do let(:config) { 'production' } diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 23270ad5053..293f112b2b0 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -33,7 +33,9 @@ describe Gitlab::Ci::Config::Entry::Global do end describe '#compose!' do - before { global.compose! } + before do + global.compose! + end it 'creates nodes hash' do expect(global.descendants).to be_an Array @@ -79,7 +81,9 @@ describe Gitlab::Ci::Config::Entry::Global do end context 'when composed' do - before { global.compose! } + before do + global.compose! + end describe '#errors' do it 'has no errors' do @@ -95,13 +99,13 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#image_value' do it 'returns valid image' do - expect(global.image_value).to eq 'ruby:2.2' + expect(global.image_value).to eq(name: 'ruby:2.2') end end describe '#services_value' do it 'returns array of services' do - expect(global.services_value).to eq ['postgres:9.1', 'mysql:5.5'] + expect(global.services_value).to eq [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }] end end @@ -150,8 +154,8 @@ describe Gitlab::Ci::Config::Entry::Global do script: %w[rspec ls], before_script: %w(ls pwd), commands: "ls\npwd\nrspec\nls", - image: 'ruby:2.2', - services: ['postgres:9.1', 'mysql:5.5'], + image: { name: 'ruby:2.2' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: { 'VAR' => 'value' }, @@ -161,8 +165,8 @@ describe Gitlab::Ci::Config::Entry::Global do before_script: [], script: %w[spinach], commands: 'spinach', - image: 'ruby:2.2', - services: ['postgres:9.1', 'mysql:5.5'], + image: { name: 'ruby:2.2' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: {}, @@ -175,7 +179,9 @@ describe Gitlab::Ci::Config::Entry::Global do end context 'when most of entires not defined' do - before { global.compose! } + before do + global.compose! + end let(:hash) do { cache: { key: 'a' }, rspec: { script: %w[ls] } } @@ -218,7 +224,9 @@ describe Gitlab::Ci::Config::Entry::Global do # details. # context 'when entires specified but not defined' do - before { global.compose! } + before do + global.compose! + end let(:hash) do { variables: nil, rspec: { script: 'rspec' } } @@ -233,7 +241,9 @@ describe Gitlab::Ci::Config::Entry::Global do end context 'when configuration is not valid' do - before { global.compose! } + before do + global.compose! + end context 'when before script is not an array' do let(:hash) do @@ -297,7 +307,9 @@ describe Gitlab::Ci::Config::Entry::Global do end describe '#[]' do - before { global.compose! } + before do + global.compose! + end let(:hash) do { cache: { key: 'a' }, rspec: { script: 'ls' } } diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 3c99cb0a1ee..bca22e39500 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -3,43 +3,104 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Image do let(:entry) { described_class.new(config) } - describe 'validation' do - context 'when entry config value is correct' do - let(:config) { 'ruby:2.2' } + context 'when configuration is a string' do + let(:config) { 'ruby:2.2' } - describe '#value' do - it 'returns image string' do - expect(entry.value).to eq 'ruby:2.2' - end + describe '#value' do + it 'returns image hash' do + expect(entry.value).to eq({ name: 'ruby:2.2' }) end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#image' do + it "returns image's name" do + expect(entry.name).to eq 'ruby:2.2' + end + end - describe '#errors' do - it 'does not append errors' do - expect(entry.errors).to be_empty - end + describe '#entrypoint' do + it "returns image's entrypoint" do + expect(entry.entrypoint).to be_nil end + end + end - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end + context 'when configuration is a hash' do + let(:config) { { name: 'ruby:2.2', entrypoint: '/bin/sh' } } + + describe '#value' do + it 'returns image hash' do + expect(entry.value).to eq(config) end end - context 'when entry value is not correct' do - let(:config) { ['ruby:2.2'] } + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'image config should be a string' - end + describe '#image' do + it "returns image's name" do + expect(entry.name).to eq 'ruby:2.2' end + end + + describe '#entrypoint' do + it "returns image's entrypoint" do + expect(entry.entrypoint).to eq '/bin/sh' + end + end + end + + context 'when entry value is not correct' do + let(:config) { ['ruby:2.2'] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'image config should be a hash or a string' + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when unexpected key is specified' do + let(:config) { { name: 'ruby:2.2', non_existing: 'test' } } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'image config contains unknown keys: non_existing' + end + end - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 9249bb9c172..92cba689f47 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -18,7 +18,9 @@ describe Gitlab::Ci::Config::Entry::Job do end describe 'validations' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry config value is correct' do let(:config) { { script: 'rspec' } } @@ -97,14 +99,16 @@ describe Gitlab::Ci::Config::Entry::Job do let(:deps) { double('deps', '[]' => unspecified) } context 'when job config overrides global config' do - before { entry.compose!(deps) } + before do + entry.compose!(deps) + end let(:config) do { script: 'rspec', image: 'some_image', cache: { key: 'test' } } end it 'overrides global config' do - expect(entry[:image].value).to eq 'some_image' + expect(entry[:image].value).to eq(name: 'some_image') expect(entry[:cache].value).to eq(key: 'test') end end @@ -125,10 +129,14 @@ describe Gitlab::Ci::Config::Entry::Job do end context 'when composed' do - before { entry.compose! } + before do + entry.compose! + end describe '#value' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry is correct' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index 7d104372ac6..c0a2b6517e3 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::Ci::Config::Entry::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } @@ -48,7 +50,9 @@ describe Gitlab::Ci::Config::Entry::Jobs do end context 'when valid job entries composed' do - before { entry.compose! } + before do + entry.compose! + end let(:config) do { rspec: { script: 'rspec' }, diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb new file mode 100644 index 00000000000..7202fe525e4 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Service do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is a string' do + let(:config) { 'postgresql:9.5' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to include(name: 'postgresql:9.5') + end + end + + describe '#image' do + it "returns service's image name" do + expect(entry.name).to eq 'postgresql:9.5' + end + end + + describe '#alias' do + it "returns service's alias" do + expect(entry.alias).to be_nil + end + end + + describe '#command' do + it "returns service's command" do + expect(entry.command).to be_nil + end + end + end + + context 'when configuration is a hash' do + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: 'cmd', entrypoint: '/bin/sh' } + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq config + end + end + + describe '#image' do + it "returns service's image name" do + expect(entry.name).to eq 'postgresql:9.5' + end + end + + describe '#alias' do + it "returns service's alias" do + expect(entry.alias).to eq 'db' + end + end + + describe '#command' do + it "returns service's command" do + expect(entry.command).to eq 'cmd' + end + end + + describe '#entrypoint' do + it "returns service's entrypoint" do + expect(entry.entrypoint).to eq '/bin/sh' + end + end + end + + context 'when entry value is not correct' do + let(:config) { ['postgresql:9.5'] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'service config should be a hash or a string' + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when unexpected key is specified' do + let(:config) { { name: 'postgresql:9.5', non_existing: 'test' } } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'service config contains unknown keys: non_existing' + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb index 66fad3b6b16..7c4319aee63 100644 --- a/spec/lib/gitlab/ci/config/entry/services_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb @@ -3,37 +3,32 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Services do let(:entry) { described_class.new(config) } - describe 'validations' do - context 'when entry config value is correct' do - let(:config) { ['postgres:9.1', 'mysql:5.5'] } + before do + entry.compose! + end - describe '#value' do - it 'returns array of services as is' do - expect(entry.value).to eq config - end - end + context 'when configuration is valid' do + let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old' }] } - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid end end - context 'when entry value is not correct' do - let(:config) { 'ls' } - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'services config should be an array of strings' - end + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old' }]) end + end + end + + context 'when configuration is invalid' do + let(:config) { 'postgresql:9.5' } - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid end end end diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index 8ad9b7cdf07..114d2490490 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -47,7 +47,9 @@ describe Gitlab::Ci::Status::Build::Cancelable do describe '#has_action?' do context 'when user is allowed to update build' do - before { build.project.team << [user, :developer] } + before do + build.project.team << [user, :developer] + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb index 72bd7c4eb93..03d1f46b517 100644 --- a/spec/lib/gitlab/ci/status/build/common_spec.rb +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -17,13 +17,17 @@ describe Gitlab::Ci::Status::Build::Common do describe '#has_details?' do context 'when user has access to read build' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it { is_expected.to have_details } end context 'when user does not have access to read build' do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.not_to have_details } end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 3f30b2c38f2..c8a97016f20 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -6,7 +6,9 @@ describe Gitlab::Ci::Status::Build::Factory do let(:status) { factory.fabricate! } let(:factory) { described_class.new(build, user) } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end context 'when build is successful' do let(:build) { create(:ci_build, :success) } diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index 0e15a5f3c6b..32b2e62e4e0 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -28,7 +28,9 @@ describe Gitlab::Ci::Status::Build::Play do end context 'when user can not push to the branch' do - before { build.project.add_developer(user) } + before do + build.project.add_developer(user) + end it { is_expected.not_to have_action } end diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 2db0f8d29bd..099d873fc01 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -47,7 +47,9 @@ describe Gitlab::Ci::Status::Build::Retryable do describe '#has_action?' do context 'when user is allowed to update build' do - before { build.project.team << [user, :developer] } + before do + build.project.team << [user, :developer] + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 8d021c35a69..23902f26b1a 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -19,7 +19,9 @@ describe Gitlab::Ci::Status::Build::Stop do describe '#has_action?' do context 'when user is allowed to update build' do - before { build.project.team << [user, :developer] } + before do + build.project.team << [user, :developer] + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb index 5a97d98b55f..b38fbee2486 100644 --- a/spec/lib/gitlab/ci/status/external/common_spec.rb +++ b/spec/lib/gitlab/ci/status/external/common_spec.rb @@ -4,9 +4,10 @@ describe Gitlab::Ci::Status::External::Common do let(:user) { create(:user) } let(:project) { external_status.project } let(:external_target_url) { 'http://example.gitlab.com/status' } + let(:external_description) { 'my description' } let(:external_status) do - create(:generic_commit_status, target_url: external_target_url) + create(:generic_commit_status, target_url: external_target_url, description: external_description) end subject do @@ -15,13 +16,21 @@ describe Gitlab::Ci::Status::External::Common do .extend(described_class) end + describe '#label' do + it 'returns description' do + expect(subject.label).to eq external_description + end + end + describe '#has_action?' do it { is_expected.not_to have_action } end describe '#has_details?' do context 'when user has access to read commit status' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it { is_expected.to have_details } end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index d665674bf70..f5fd31e8d03 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -17,7 +17,9 @@ describe Gitlab::Ci::Status::Pipeline::Common do describe '#has_details?' do context 'when user has access to read pipeline' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it { is_expected.to have_details } end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 97af1c2523d..ca68010cb89 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -306,58 +306,58 @@ describe Gitlab::ClosingIssueExtractor, lib: true do it 'fetches issues in single line message' do message = "Closes #{reference} and fix #{reference2}" - expect(subject.closed_by_message(message)). - to match_array([issue, other_issue]) + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue]) end it 'fetches comma-separated issues references in single line message' do message = "Closes #{reference}, closes #{reference2}" - expect(subject.closed_by_message(message)). - to match_array([issue, other_issue]) + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue]) end it 'fetches comma-separated issues numbers in single line message' do message = "Closes #{reference}, #{reference2} and #{reference3}" - expect(subject.closed_by_message(message)). - to match_array([issue, other_issue, third_issue]) + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) end it 'fetches issues in multi-line message' do message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" - expect(subject.closed_by_message(message)). - to match_array([issue, other_issue]) + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue]) end it 'fetches issues in hybrid message' do message = "Awesome commit (closes #{reference})\n"\ "Also fixing issues #{reference2}, #{reference3} and #4" - expect(subject.closed_by_message(message)). - to match_array([issue, other_issue, third_issue]) + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) end it "fetches cross-project references" do message = "Closes #{reference} and #{cross_reference}" - expect(subject.closed_by_message(message)). - to match_array([issue, issue2]) + expect(subject.closed_by_message(message)) + .to match_array([issue, issue2]) end it "fetches cross-project URL references" do message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)} and #{reference}" - expect(subject.closed_by_message(message)). - to match_array([issue, issue2]) + expect(subject.closed_by_message(message)) + .to match_array([issue, issue2]) end it "ignores invalid cross-project URL references" do message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)} and #{reference}" - expect(subject.closed_by_message(message)). - to match_array([issue]) + expect(subject.closed_by_message(message)) + .to match_array([issue]) end end end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 780ac0ad97e..585eeb77bd5 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -43,8 +43,8 @@ describe Gitlab::Conflict::File, lib: true do end it 'returns a file containing only the chosen parts of the resolved sections' do - expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)). - to eq(%w(both new both old both new both)) + expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)) + .to eq(%w(both new both old both new both)) end end @@ -52,14 +52,14 @@ describe Gitlab::Conflict::File, lib: true do empty_hash = section_keys.map { |key| [key, nil] }.to_h invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h - expect { conflict_file.resolve_lines({}) }. - to raise_error(Gitlab::Conflict::File::MissingResolution) + expect { conflict_file.resolve_lines({}) } + .to raise_error(Gitlab::Conflict::File::MissingResolution) - expect { conflict_file.resolve_lines(empty_hash) }. - to raise_error(Gitlab::Conflict::File::MissingResolution) + expect { conflict_file.resolve_lines(empty_hash) } + .to raise_error(Gitlab::Conflict::File::MissingResolution) - expect { conflict_file.resolve_lines(invalid_hash) }. - to raise_error(Gitlab::Conflict::File::MissingResolution) + expect { conflict_file.resolve_lines(invalid_hash) } + .to raise_error(Gitlab::Conflict::File::MissingResolution) end end @@ -250,8 +250,8 @@ FILE describe '#as_json' do it 'includes the blob path for the file' do - expect(conflict_file.as_json[:blob_path]). - to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb") + expect(conflict_file.as_json[:blob_path]) + .to eq("/#{project.full_path}/blob/#{our_commit.oid}/files/ruby/regex.rb") end it 'includes the blob icon for the file' do @@ -264,8 +264,8 @@ FILE end it 'includes the detected language of the conflict file' do - expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]). - to eq('ruby') + expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]) + .to eq('ruby') end end end diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb index 2570f95dd21..aed57b75789 100644 --- a/spec/lib/gitlab/conflict/parser_spec.rb +++ b/spec/lib/gitlab/conflict/parser_spec.rb @@ -122,18 +122,18 @@ CONFLICT context 'when the file contents include conflict delimiters' do context 'when there is a non-start delimiter first' do it 'raises UnexpectedDelimiter when there is a middle delimiter first' do - expect { parse_text('=======') }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + expect { parse_text('=======') } + .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when there is an end delimiter first' do - expect { parse_text('>>>>>>> README.md') }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + expect { parse_text('>>>>>>> README.md') } + .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when there is an end delimiter for a different path first' do - expect { parse_text('>>>>>>> some-other-path.md') }. - not_to raise_error + expect { parse_text('>>>>>>> some-other-path.md') } + .not_to raise_error end end @@ -142,18 +142,18 @@ CONFLICT let(:end_text) { "\n=======\n>>>>>>> README.md" } it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do - expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + expect { parse_text(start_text + '>>>>>>> README.md' + end_text) } + .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do - expect { parse_text(start_text + start_text + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + expect { parse_text(start_text + start_text + end_text) } + .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when it is followed by a start delimiter for a different path' do - expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }. - not_to raise_error + expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) } + .not_to raise_error end end @@ -162,59 +162,59 @@ CONFLICT let(:end_text) { "\n>>>>>>> README.md" } it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do - expect { parse_text(start_text + '=======' + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + expect { parse_text(start_text + '=======' + end_text) } + .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do - expect { parse_text(start_text + start_text + end_text) }. - to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + expect { parse_text(start_text + start_text + end_text) } + .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when it is followed by a start delimiter for another path' do - expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) }. - not_to raise_error + expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) } + .not_to raise_error end end it 'raises MissingEndDelimiter when there is no end delimiter at the end' do start_text = "<<<<<<< README.md\n=======\n" - expect { parse_text(start_text) }. - to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) + expect { parse_text(start_text) } + .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) - expect { parse_text(start_text + '>>>>>>> some-other-path.md') }. - to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) + expect { parse_text(start_text + '>>>>>>> some-other-path.md') } + .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) end end context 'other file types' do it 'raises UnmergeableFile when lines is blank, indicating a binary file' do - expect { parse_text('') }. - to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + expect { parse_text('') } + .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) - expect { parse_text(nil) }. - to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + expect { parse_text(nil) } + .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) end it 'raises UnmergeableFile when the file is over 200 KB' do - expect { parse_text('a' * 204801) }. - to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + expect { parse_text('a' * 204801) } + .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) end # All text from Rugged has an encoding of ASCII_8BIT, so force that in # these strings. context 'when the file contains UTF-8 characters' do it 'does not raise' do - expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }. - not_to raise_error + expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) } + .not_to raise_error end end context 'when the file contains non-UTF-8 characters' do it 'raises UnsupportedEncoding' do - expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }. - to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) + expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) } + .to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) end end end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index fda39d78610..a566f24f6a6 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -32,6 +32,37 @@ describe Gitlab::CurrentSettings do expect(current_application_settings).to be_a(ApplicationSetting) end + + context 'with migrations pending' do + before do + expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true) + end + + it 'returns an in-memory ApplicationSetting object' do + settings = current_application_settings + + expect(settings).to be_a(OpenStruct) + expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled) + expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled) + end + + it 'uses the existing database settings and falls back to defaults' do + db_settings = create(:application_setting, + home_page_url: 'http://mydomain.com', + signup_enabled: false) + settings = current_application_settings + app_defaults = ApplicationSetting.last + + expect(settings).to be_a(OpenStruct) + expect(settings.home_page_url).to eq(db_settings.home_page_url) + expect(settings.signup_enabled?).to be_falsey + expect(settings.signup_enabled).to be_falsey + + # Check that unspecified values use the defaults + settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key } + settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) } + end + end end context 'with DB unavailable' do diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index e59cba35b2f..73936969832 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -47,8 +47,8 @@ describe Gitlab::DataBuilder::Push, lib: true do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, spy, nil) }. - not_to raise_error + expect { described_class.build(spy, spy, spy, spy, spy, nil) } + .not_to raise_error end end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 30aa463faf8..4259be3f522 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -57,15 +57,15 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end it 'creates the index concurrently' do - expect(model).to receive(:add_index). - with(:users, :foo, algorithm: :concurrently) + expect(model).to receive(:add_index) + .with(:users, :foo, algorithm: :concurrently) model.add_concurrent_index(:users, :foo) end it 'creates unique index concurrently' do - expect(model).to receive(:add_index). - with(:users, :foo, { algorithm: :concurrently, unique: true }) + expect(model).to receive(:add_index) + .with(:users, :foo, { algorithm: :concurrently, unique: true }) model.add_concurrent_index(:users, :foo, unique: true) end @@ -75,8 +75,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'creates a regular index' do expect(Gitlab::Database).to receive(:postgresql?).and_return(false) - expect(model).to receive(:add_index). - with(:users, :foo, {}) + expect(model).to receive(:add_index) + .with(:users, :foo, {}) model.add_concurrent_index(:users, :foo) end @@ -87,8 +87,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'raises RuntimeError' do expect(model).to receive(:transaction_open?).and_return(true) - expect { model.add_concurrent_index(:users, :foo) }. - to raise_error(RuntimeError) + expect { model.add_concurrent_index(:users, :foo) } + .to raise_error(RuntimeError) end end end @@ -106,15 +106,15 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end it 'removes the index concurrently by column name' do - expect(model).to receive(:remove_index). - with(:users, { algorithm: :concurrently, column: :foo }) + expect(model).to receive(:remove_index) + .with(:users, { algorithm: :concurrently, column: :foo }) model.remove_concurrent_index(:users, :foo) end it 'removes the index concurrently by index name' do - expect(model).to receive(:remove_index). - with(:users, { algorithm: :concurrently, name: "index_x_by_y" }) + expect(model).to receive(:remove_index) + .with(:users, { algorithm: :concurrently, name: "index_x_by_y" }) model.remove_concurrent_index_by_name(:users, "index_x_by_y") end @@ -124,8 +124,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'removes an index' do expect(Gitlab::Database).to receive(:postgresql?).and_return(false) - expect(model).to receive(:remove_index). - with(:users, { column: :foo }) + expect(model).to receive(:remove_index) + .with(:users, { column: :foo }) model.remove_concurrent_index(:users, :foo) end @@ -136,8 +136,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'raises RuntimeError' do expect(model).to receive(:transaction_open?).and_return(true) - expect { model.remove_concurrent_index(:users, :foo) }. - to raise_error(RuntimeError) + expect { model.remove_concurrent_index(:users, :foo) } + .to raise_error(RuntimeError) end end end @@ -162,8 +162,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'creates a regular foreign key' do allow(Gitlab::Database).to receive(:mysql?).and_return(true) - expect(model).to receive(:add_foreign_key). - with(:projects, :users, column: :user_id, on_delete: :cascade) + expect(model).to receive(:add_foreign_key) + .with(:projects, :users, column: :user_id, on_delete: :cascade) model.add_concurrent_foreign_key(:projects, :users, column: :user_id) end @@ -262,39 +262,53 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end describe '#update_column_in_batches' do - before do - create_list(:empty_project, 5) - end + context 'when running outside of a transaction' do + before do + expect(model).to receive(:transaction_open?).and_return(false) - it 'updates all the rows in a table' do - model.update_column_in_batches(:projects, :import_error, 'foo') + create_list(:empty_project, 5) + end - expect(Project.where(import_error: 'foo').count).to eq(5) - end + it 'updates all the rows in a table' do + model.update_column_in_batches(:projects, :import_error, 'foo') + + expect(Project.where(import_error: 'foo').count).to eq(5) + end - it 'updates boolean values correctly' do - model.update_column_in_batches(:projects, :archived, true) + it 'updates boolean values correctly' do + model.update_column_in_batches(:projects, :archived, true) - expect(Project.where(archived: true).count).to eq(5) - end + expect(Project.where(archived: true).count).to eq(5) + end - context 'when a block is supplied' do - it 'yields an Arel table and query object to the supplied block' do - first_id = Project.first.id + context 'when a block is supplied' do + it 'yields an Arel table and query object to the supplied block' do + first_id = Project.first.id - model.update_column_in_batches(:projects, :archived, true) do |t, query| - query.where(t[:id].eq(first_id)) + model.update_column_in_batches(:projects, :archived, true) do |t, query| + query.where(t[:id].eq(first_id)) + end + + expect(Project.where(archived: true).count).to eq(1) end + end + + context 'when the value is Arel.sql (Arel::Nodes::SqlLiteral)' do + it 'updates the value as a SQL expression' do + model.update_column_in_batches(:projects, :star_count, Arel.sql('1+1')) - expect(Project.where(archived: true).count).to eq(1) + expect(Project.sum(:star_count)).to eq(2 * Project.count) + end end end - context 'when the value is Arel.sql (Arel::Nodes::SqlLiteral)' do - it 'updates the value as a SQL expression' do - model.update_column_in_batches(:projects, :star_count, Arel.sql('1+1')) + context 'when running inside the transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) - expect(Project.sum(:star_count)).to eq(2 * Project.count) + expect do + model.update_column_in_batches(:projects, :star_count, Arel.sql('1+1')) + end.to raise_error(RuntimeError) end end end @@ -303,20 +317,22 @@ describe Gitlab::Database::MigrationHelpers, lib: true do context 'outside of a transaction' do context 'when a column limit is not set' do before do - expect(model).to receive(:transaction_open?).and_return(false) + expect(model).to receive(:transaction_open?) + .and_return(false) + .at_least(:once) expect(model).to receive(:transaction).and_yield - expect(model).to receive(:add_column). - with(:projects, :foo, :integer, default: nil) + expect(model).to receive(:add_column) + .with(:projects, :foo, :integer, default: nil) - expect(model).to receive(:change_column_default). - with(:projects, :foo, 10) + expect(model).to receive(:change_column_default) + .with(:projects, :foo, 10) end it 'adds the column while allowing NULL values' do - expect(model).to receive(:update_column_in_batches). - with(:projects, :foo, 10) + expect(model).to receive(:update_column_in_batches) + .with(:projects, :foo, 10) expect(model).not_to receive(:change_column_null) @@ -326,22 +342,22 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end it 'adds the column while not allowing NULL values' do - expect(model).to receive(:update_column_in_batches). - with(:projects, :foo, 10) + expect(model).to receive(:update_column_in_batches) + .with(:projects, :foo, 10) - expect(model).to receive(:change_column_null). - with(:projects, :foo, false) + expect(model).to receive(:change_column_null) + .with(:projects, :foo, false) model.add_column_with_default(:projects, :foo, :integer, default: 10) end it 'removes the added column whenever updating the rows fails' do - expect(model).to receive(:update_column_in_batches). - with(:projects, :foo, 10). - and_raise(RuntimeError) + expect(model).to receive(:update_column_in_batches) + .with(:projects, :foo, 10) + .and_raise(RuntimeError) - expect(model).to receive(:remove_column). - with(:projects, :foo) + expect(model).to receive(:remove_column) + .with(:projects, :foo) expect do model.add_column_with_default(:projects, :foo, :integer, default: 10) @@ -349,12 +365,12 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end it 'removes the added column whenever changing a column NULL constraint fails' do - expect(model).to receive(:change_column_null). - with(:projects, :foo, false). - and_raise(RuntimeError) + expect(model).to receive(:change_column_null) + .with(:projects, :foo, false) + .and_raise(RuntimeError) - expect(model).to receive(:remove_column). - with(:projects, :foo) + expect(model).to receive(:remove_column) + .with(:projects, :foo) expect do model.add_column_with_default(:projects, :foo, :integer, default: 10) @@ -370,8 +386,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do allow(model).to receive(:change_column_null).with(:projects, :foo, false) allow(model).to receive(:change_column_default).with(:projects, :foo, 10) - expect(model).to receive(:add_column). - with(:projects, :foo, :integer, default: nil, limit: 8) + expect(model).to receive(:add_column) + .with(:projects, :foo, :integer, default: nil, limit: 8) model.add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8) end @@ -394,8 +410,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'raises RuntimeError' do allow(model).to receive(:transaction_open?).and_return(true) - expect { model.rename_column_concurrently(:users, :old, :new) }. - to raise_error(RuntimeError) + expect { model.rename_column_concurrently(:users, :old, :new) } + .to raise_error(RuntimeError) end end @@ -426,17 +442,17 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'renames a column concurrently' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) - expect(model).to receive(:install_rename_triggers_for_mysql). - with(trigger_name, 'users', 'old', 'new') + expect(model).to receive(:install_rename_triggers_for_mysql) + .with(trigger_name, 'users', 'old', 'new') - expect(model).to receive(:add_column). - with(:users, :new, :integer, + expect(model).to receive(:add_column) + .with(:users, :new, :integer, limit: old_column.limit, precision: old_column.precision, scale: old_column.scale) - expect(model).to receive(:change_column_default). - with(:users, :new, old_column.default) + expect(model).to receive(:change_column_default) + .with(:users, :new, old_column.default) expect(model).to receive(:update_column_in_batches) @@ -453,17 +469,17 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'renames a column concurrently' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) - expect(model).to receive(:install_rename_triggers_for_postgresql). - with(trigger_name, 'users', 'old', 'new') + expect(model).to receive(:install_rename_triggers_for_postgresql) + .with(trigger_name, 'users', 'old', 'new') - expect(model).to receive(:add_column). - with(:users, :new, :integer, + expect(model).to receive(:add_column) + .with(:users, :new, :integer, limit: old_column.limit, precision: old_column.precision, scale: old_column.scale) - expect(model).to receive(:change_column_default). - with(:users, :new, old_column.default) + expect(model).to receive(:change_column_default) + .with(:users, :new, old_column.default) expect(model).to receive(:update_column_in_batches) @@ -482,8 +498,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'cleans up the renaming procedure for PostgreSQL' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) - expect(model).to receive(:remove_rename_triggers_for_postgresql). - with(:users, /trigger_.{12}/) + expect(model).to receive(:remove_rename_triggers_for_postgresql) + .with(:users, /trigger_.{12}/) expect(model).to receive(:remove_column).with(:users, :old) @@ -493,8 +509,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do it 'cleans up the renaming procedure for MySQL' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) - expect(model).to receive(:remove_rename_triggers_for_mysql). - with(/trigger_.{12}/) + expect(model).to receive(:remove_rename_triggers_for_mysql) + .with(/trigger_.{12}/) expect(model).to receive(:remove_column).with(:users, :old) @@ -504,8 +520,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#change_column_type_concurrently' do it 'changes the column type' do - expect(model).to receive(:rename_column_concurrently). - with('users', 'username', 'username_for_type_change', type: :text) + expect(model).to receive(:rename_column_concurrently) + .with('users', 'username', 'username_for_type_change', type: :text) model.change_column_type_concurrently('users', 'username', :text) end @@ -513,11 +529,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#cleanup_concurrent_column_type_change' do it 'cleans up the type changing procedure' do - expect(model).to receive(:cleanup_concurrent_column_rename). - with('users', 'username', 'username_for_type_change') + expect(model).to receive(:cleanup_concurrent_column_rename) + .with('users', 'username', 'username_for_type_change') - expect(model).to receive(:rename_column). - with('users', 'username_for_type_change', 'username') + expect(model).to receive(:rename_column) + .with('users', 'username_for_type_change', 'username') model.cleanup_concurrent_column_type_change('users', 'username') end @@ -525,11 +541,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#install_rename_triggers_for_postgresql' do it 'installs the triggers for PostgreSQL' do - expect(model).to receive(:execute). - with(/CREATE OR REPLACE FUNCTION foo()/m) + expect(model).to receive(:execute) + .with(/CREATE OR REPLACE FUNCTION foo()/m) - expect(model).to receive(:execute). - with(/CREATE TRIGGER foo/m) + expect(model).to receive(:execute) + .with(/CREATE TRIGGER foo/m) model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) end @@ -537,11 +553,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#install_rename_triggers_for_mysql' do it 'installs the triggers for MySQL' do - expect(model).to receive(:execute). - with(/CREATE TRIGGER foo_insert.+ON users/m) + expect(model).to receive(:execute) + .with(/CREATE TRIGGER foo_insert.+ON users/m) - expect(model).to receive(:execute). - with(/CREATE TRIGGER foo_update.+ON users/m) + expect(model).to receive(:execute) + .with(/CREATE TRIGGER foo_update.+ON users/m) model.install_rename_triggers_for_mysql('foo', :users, :old, :new) end @@ -567,8 +583,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do describe '#rename_trigger_name' do it 'returns a String' do - expect(model.rename_trigger_name(:users, :foo, :bar)). - to match(/trigger_.{12}/) + expect(model.rename_trigger_name(:users, :foo, :bar)) + .to match(/trigger_.{12}/) end end @@ -607,11 +623,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do lengths: [], orders: []) - allow(model).to receive(:indexes_for).with(:issues, 'project_id'). - and_return([index]) + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) - expect(model).to receive(:add_concurrent_index). - with(:issues, + expect(model).to receive(:add_concurrent_index) + .with(:issues, %w(gl_project_id), unique: false, name: 'index_on_issues_gl_project_id', @@ -634,11 +650,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do lengths: [], orders: []) - allow(model).to receive(:indexes_for).with(:issues, 'project_id'). - and_return([index]) + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) - expect(model).to receive(:add_concurrent_index). - with(:issues, + expect(model).to receive(:add_concurrent_index) + .with(:issues, %w(gl_project_id foobar), unique: false, name: 'index_on_issues_gl_project_id_foobar', @@ -661,11 +677,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do lengths: [], orders: []) - allow(model).to receive(:indexes_for).with(:issues, 'project_id'). - and_return([index]) + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) - expect(model).to receive(:add_concurrent_index). - with(:issues, + expect(model).to receive(:add_concurrent_index) + .with(:issues, %w(gl_project_id), unique: false, name: 'index_on_issues_gl_project_id', @@ -689,11 +705,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do lengths: [], orders: []) - allow(model).to receive(:indexes_for).with(:issues, 'project_id'). - and_return([index]) + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) - expect(model).to receive(:add_concurrent_index). - with(:issues, + expect(model).to receive(:add_concurrent_index) + .with(:issues, %w(gl_project_id), unique: false, name: 'index_on_issues_gl_project_id', @@ -717,11 +733,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do lengths: [], orders: []) - allow(model).to receive(:indexes_for).with(:issues, 'project_id'). - and_return([index]) + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) - expect(model).to receive(:add_concurrent_index). - with(:issues, + expect(model).to receive(:add_concurrent_index) + .with(:issues, %w(gl_project_id), unique: false, name: 'index_on_issues_gl_project_id', @@ -745,11 +761,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do lengths: [], orders: []) - allow(model).to receive(:indexes_for).with(:issues, 'project_id'). - and_return([index]) + allow(model).to receive(:indexes_for).with(:issues, 'project_id') + .and_return([index]) - expect { model.copy_indexes(:issues, :project_id, :gl_project_id) }. - to raise_error(RuntimeError) + expect { model.copy_indexes(:issues, :project_id, :gl_project_id) } + .to raise_error(RuntimeError) end end end @@ -761,11 +777,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do to_table: 'projects', on_delete: :cascade) - allow(model).to receive(:foreign_keys_for).with(:issues, :project_id). - and_return([fk]) + allow(model).to receive(:foreign_keys_for).with(:issues, :project_id) + .and_return([fk]) - expect(model).to receive(:add_concurrent_foreign_key). - with('issues', 'projects', column: :gl_project_id, on_delete: :cascade) + expect(model).to receive(:add_concurrent_foreign_key) + .with('issues', 'projects', column: :gl_project_id, on_delete: :cascade) model.copy_foreign_keys(:issues, :project_id, :gl_project_id) end @@ -790,8 +806,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end it 'builds the sql with correct functions' do - expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s). - to include('regexp_replace') + expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s) + .to include('regexp_replace') end end @@ -801,8 +817,8 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end it 'builds the sql with the correct functions' do - expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s). - to include('locate', 'insert') + expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s) + .to include('locate', 'insert') end end @@ -810,7 +826,11 @@ describe Gitlab::Database::MigrationHelpers, lib: true do let!(:user) { create(:user, name: 'Kathy Alice Aliceson') } it 'replaces the correct part of the string' do - model.update_column_in_batches(:users, :name, model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve')) + allow(model).to receive(:transaction_open?).and_return(false) + query = model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve') + + model.update_column_in_batches(:users, :name, query) + expect(user.reload.name).to eq('Kathy Eve Aliceson') end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index a3ab4e3dd9e..5653cfee686 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :truncate do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index ce2b5d620fd..8125dedd3fc 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :truncate do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } @@ -21,8 +21,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do parent = create(:group, path: 'parent') child = create(:group, path: 'the-path', parent: parent) - found_ids = subject.namespaces_for_paths(type: :child). - map(&:id) + found_ids = subject.namespaces_for_paths(type: :child) + .map(&:id) expect(found_ids).to contain_exactly(child.id) end @@ -38,8 +38,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do path: 'the-path', parent: create(:group)) - found_ids = subject.namespaces_for_paths(type: :child). - map(&:id) + found_ids = subject.namespaces_for_paths(type: :child) + .map(&:id) expect(found_ids).to contain_exactly(namespace.id) end @@ -53,8 +53,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do path: 'the-path', parent: create(:group)) - found_ids = subject.namespaces_for_paths(type: :child). - map(&:id) + found_ids = subject.namespaces_for_paths(type: :child) + .map(&:id) expect(found_ids).to contain_exactly(namespace.id) end @@ -68,8 +68,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do path: 'the-path', parent: create(:group)) - found_ids = subject.namespaces_for_paths(type: :top_level). - map(&:id) + found_ids = subject.namespaces_for_paths(type: :top_level) + .map(&:id) expect(found_ids).to contain_exactly(root_namespace.id) end @@ -81,8 +81,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do path: 'the-path', parent: create(:group)) - found_ids = subject.namespaces_for_paths(type: :top_level). - map(&:id) + found_ids = subject.namespaces_for_paths(type: :top_level) + .map(&:id) expect(found_ids).to contain_exactly(root_namespace.id) end @@ -140,9 +140,9 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do let(:namespace) { create(:group, name: 'the-path') } it 'renames paths & routes for the namespace' do - expect(subject).to receive(:rename_path_for_routable). - with(namespace). - and_call_original + expect(subject).to receive(:rename_path_for_routable) + .with(namespace) + .and_call_original subject.rename_namespace(namespace) @@ -211,15 +211,15 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do end it 'renames top level namespaces the namespace' do - expect(subject).to receive(:rename_namespace). - with(migration_namespace(top_level_namespace)) + expect(subject).to receive(:rename_namespace) + .with(migration_namespace(top_level_namespace)) subject.rename_namespaces(type: :top_level) end it 'renames child namespaces' do - expect(subject).to receive(:rename_namespace). - with(migration_namespace(child_namespace)) + expect(subject).to receive(:rename_namespace) + .with(migration_namespace(child_namespace)) subject.rename_namespaces(type: :child) end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index 59e8de2712d..802f77ad430 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :truncate do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } @@ -13,8 +13,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do namespace = create(:namespace, path: 'hello') project = create(:empty_project, path: 'THE-path', namespace: namespace) - result_ids = described_class.new(['Hello/the-path'], migration). - projects_for_paths.map(&:id) + result_ids = described_class.new(['Hello/the-path'], migration) + .projects_for_paths.map(&:id) expect(result_ids).to contain_exactly(project.id) end @@ -39,8 +39,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do end it 'invalidates the markdown cache of related projects' do - expect(subject).to receive(:remove_cached_html_for_projects). - with(projects.map(&:id)) + expect(subject).to receive(:remove_cached_html_for_projects) + .with(projects.map(&:id)) subject.rename_projects end @@ -54,9 +54,9 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do end it 'renames path & route for the project' do - expect(subject).to receive(:rename_path_for_routable). - with(project). - and_call_original + expect(subject).to receive(:rename_path_for_routable) + .with(project) + .and_call_original subject.rename_project(project) @@ -64,24 +64,24 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do end it 'moves the wiki & the repo' do - expect(subject).to receive(:move_repository). - with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki') - expect(subject).to receive(:move_repository). - with(project, 'known-parent/the-path', 'known-parent/the-path0') + expect(subject).to receive(:move_repository) + .with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki') + expect(subject).to receive(:move_repository) + .with(project, 'known-parent/the-path', 'known-parent/the-path0') subject.rename_project(project) end it 'moves uploads' do - expect(subject).to receive(:move_uploads). - with('known-parent/the-path', 'known-parent/the-path0') + expect(subject).to receive(:move_uploads) + .with('known-parent/the-path', 'known-parent/the-path0') subject.rename_project(project) end it 'moves pages' do - expect(subject).to receive(:move_pages). - with('known-parent/the-path', 'known-parent/the-path0') + expect(subject).to receive(:move_pages) + .with('known-parent/the-path', 'known-parent/the-path0') subject.rename_project(project) end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb index f8cc1eb91ec..1d5e58855c1 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' shared_examples 'renames child namespaces' do |type| it 'renames namespaces' do rename_namespaces = double - expect(described_class::RenameNamespaces). - to receive(:new).with(['first-path', 'second-path'], subject). - and_return(rename_namespaces) - expect(rename_namespaces).to receive(:rename_namespaces). - with(type: :child) + expect(described_class::RenameNamespaces) + .to receive(:new).with(['first-path', 'second-path'], subject) + .and_return(rename_namespaces) + expect(rename_namespaces).to receive(:rename_namespaces) + .with(type: :child) subject.rename_wildcard_paths(['first-path', 'second-path']) end end -describe Gitlab::Database::RenameReservedPathsMigration::V1 do +describe Gitlab::Database::RenameReservedPathsMigration::V1, :truncate do let(:subject) { FakeRenameReservedPathMigrationV1.new } before do @@ -29,9 +29,9 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1 do it 'should rename projects' do rename_projects = double - expect(described_class::RenameProjects). - to receive(:new).with(['the-path'], subject). - and_return(rename_projects) + expect(described_class::RenameProjects) + .to receive(:new).with(['the-path'], subject) + .and_return(rename_projects) expect(rename_projects).to receive(:rename_projects) @@ -42,11 +42,11 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1 do describe '#rename_root_paths' do it 'should rename namespaces' do rename_namespaces = double - expect(described_class::RenameNamespaces). - to receive(:new).with(['the-path'], subject). - and_return(rename_namespaces) - expect(rename_namespaces).to receive(:rename_namespaces). - with(type: :top_level) + expect(described_class::RenameNamespaces) + .to receive(:new).with(['the-path'], subject) + .and_return(rename_namespaces) + expect(rename_namespaces).to receive(:rename_namespaces) + .with(type: :top_level) subject.rename_root_paths('the-path') end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 9b1d66a1b1c..cbf6c35356e 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -34,8 +34,8 @@ describe Gitlab::Database, lib: true do describe '.version' do context "on mysql" do it "extracts the version number" do - allow(described_class).to receive(:database_version). - and_return("5.7.12-standard") + allow(described_class).to receive(:database_version) + .and_return("5.7.12-standard") expect(described_class.version).to eq '5.7.12-standard' end @@ -43,8 +43,8 @@ describe Gitlab::Database, lib: true do context "on postgresql" do it "extracts the version number" do - allow(described_class).to receive(:database_version). - and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0") + allow(described_class).to receive(:database_version) + .and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0") expect(described_class.version).to eq '9.4.4' end @@ -53,14 +53,18 @@ describe Gitlab::Database, lib: true do describe '.nulls_last_order' do context 'when using PostgreSQL' do - before { expect(described_class).to receive(:postgresql?).and_return(true) } + before do + expect(described_class).to receive(:postgresql?).and_return(true) + end it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'} it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'} end context 'when using MySQL' do - before { expect(described_class).to receive(:postgresql?).and_return(false) } + before do + expect(described_class).to receive(:postgresql?).and_return(false) + end it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column IS NULL, column ASC'} it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC'} @@ -69,14 +73,18 @@ describe Gitlab::Database, lib: true do describe '.nulls_first_order' do context 'when using PostgreSQL' do - before { expect(described_class).to receive(:postgresql?).and_return(true) } + before do + expect(described_class).to receive(:postgresql?).and_return(true) + end it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'} it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'} end context 'when using MySQL' do - before { expect(described_class).to receive(:postgresql?).and_return(false) } + before do + expect(described_class).to receive(:postgresql?).and_return(false) + end it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'} it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'} @@ -121,6 +129,59 @@ describe Gitlab::Database, lib: true do end end + describe '.bulk_insert' do + before do + allow(described_class).to receive(:connection).and_return(connection) + allow(connection).to receive(:quote_column_name, &:itself) + allow(connection).to receive(:quote, &:itself) + allow(connection).to receive(:execute) + end + + let(:connection) { double(:connection) } + + let(:rows) do + [ + { a: 1, b: 2, c: 3 }, + { c: 6, a: 4, b: 5 } + ] + end + + it 'does nothing with empty rows' do + expect(connection).not_to receive(:execute) + + described_class.bulk_insert('test', []) + end + + it 'uses the ordering from the first row' do + expect(connection).to receive(:execute) do |sql| + expect(sql).to include('(1, 2, 3)') + expect(sql).to include('(4, 5, 6)') + end + + described_class.bulk_insert('test', rows) + end + + it 'quotes column names' do + expect(connection).to receive(:quote_column_name).with(:a) + expect(connection).to receive(:quote_column_name).with(:b) + expect(connection).to receive(:quote_column_name).with(:c) + + described_class.bulk_insert('test', rows) + end + + it 'quotes values' do + 1.upto(6) do |i| + expect(connection).to receive(:quote).with(i) + end + + described_class.bulk_insert('test', rows) + end + + it 'handles non-UTF-8 data' do + expect { described_class.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error + end + end + describe '.create_connection_pool' do it 'creates a new connection pool with specific pool size' do pool = described_class.create_connection_pool(5) diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb index 4da8821726c..7e32770f95d 100644 --- a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb @@ -54,6 +54,8 @@ describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do Sphinx>=1.3 docutils>=0.7 markupsafe + pytest~=3.0 + foop!=3.0 CONTENT end @@ -78,6 +80,8 @@ describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do expect(subject).to include(link('Sphinx', 'https://pypi.python.org/pypi/Sphinx')) expect(subject).to include(link('docutils', 'https://pypi.python.org/pypi/docutils')) expect(subject).to include(link('markupsafe', 'https://pypi.python.org/pypi/markupsafe')) + expect(subject).to include(link('pytest', 'https://pypi.python.org/pypi/pytest')) + expect(subject).to include(link('foop', 'https://pypi.python.org/pypi/foop')) end it 'links URLs' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index a9953bb0d01..f289131cc3a 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -92,4 +92,305 @@ describe Gitlab::Diff::File, lib: true do expect(diff_file.diffable?).to be_falsey end end + + describe '#content_changed?' do + context 'when created' do + let(:commit) { project.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + it 'returns false' do + expect(diff_file.content_changed?).to be_falsey + end + end + + context 'when deleted' do + let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') } + let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') } + + it 'returns false' do + expect(diff_file.content_changed?).to be_falsey + end + end + + context 'when renamed' do + let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') } + + before do + allow(diff_file.new_blob).to receive(:id).and_return(diff_file.old_blob.id) + end + + it 'returns false' do + expect(diff_file.content_changed?).to be_falsey + end + end + + context 'when content changed' do + context 'when binary' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + it 'returns true' do + expect(diff_file.content_changed?).to be_truthy + end + end + + context 'when not binary' do + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + it 'returns true' do + expect(diff_file.content_changed?).to be_truthy + end + end + end + end + + describe '#simple_viewer' do + context 'when the file is not diffable' do + before do + allow(diff_file).to receive(:diffable?).and_return(false) + end + + it 'returns a Not Diffable viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::NotDiffable) + end + end + + context 'when the content changed' do + context 'when the file represented by the diff file is binary' do + before do + allow(diff_file).to receive(:raw_binary?).and_return(true) + end + + it 'returns a No Preview viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::NoPreview) + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns a No Preview viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::NoPreview) + end + end + + context 'when the file represented by the diff file is text-based' do + it 'returns a text viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Text) + end + end + end + + context 'when created' do + let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + end + + context 'when the file represented by the diff file is binary' do + before do + allow(diff_file).to receive(:raw_binary?).and_return(true) + end + + it 'returns an Added viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Added) + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns an Added viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Added) + end + end + + context 'when the file represented by the diff file is text-based' do + it 'returns a text viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Text) + end + end + end + + context 'when deleted' do + let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') } + let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') } + + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + end + + context 'when the file represented by the diff file is binary' do + before do + allow(diff_file).to receive(:raw_binary?).and_return(true) + end + + it 'returns a Deleted viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Deleted) + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns a Deleted viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Deleted) + end + end + + context 'when the file represented by the diff file is text-based' do + it 'returns a text viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Text) + end + end + end + + context 'when renamed' do + let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') } + + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + end + + it 'returns a Renamed viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Renamed) + end + end + + context 'when mode changed' do + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + allow(diff_file).to receive(:mode_changed?).and_return(true) + end + + it 'returns a Mode Changed viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::ModeChanged) + end + end + end + + describe '#rich_viewer' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + context 'when the diff file has a matching viewer' do + context 'when the diff file content did not change' do + before do + allow(diff_file).to receive(:content_changed?).and_return(false) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when the diff file is not diffable' do + before do + allow(diff_file).to receive(:diffable?).and_return(false) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when the diff file has an external storage error' do + before do + allow(diff_file).to receive(:external_storage_error?).and_return(true) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when everything is right' do + it 'returns the viewer' do + expect(diff_file.rich_viewer).to be_a(DiffViewer::Image) + end + end + end + + context 'when the diff file does not have a matching viewer' do + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + end + + describe '#rendered_as_text?' do + context 'when the simple viewer is text-based' do + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + context 'when ignoring errors' do + context 'when the viewer has render errors' do + before do + diff_file.diff.too_large! + end + + it 'returns true' do + expect(diff_file.rendered_as_text?).to be_truthy + end + end + + context "when the viewer doesn't have render errors" do + it 'returns true' do + expect(diff_file.rendered_as_text?).to be_truthy + end + end + end + + context 'when not ignoring errors' do + context 'when the viewer has render errors' do + before do + diff_file.diff.too_large! + end + + it 'returns false' do + expect(diff_file.rendered_as_text?(ignore_errors: false)).to be_falsey + end + end + + context "when the viewer doesn't have render errors" do + it 'returns true' do + expect(diff_file.rendered_as_text?(ignore_errors: false)).to be_truthy + end + end + end + end + + context 'when the simple viewer is binary' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + it 'returns false' do + expect(diff_file.rendered_as_text?).to be_falsey + end + end + end end diff --git a/spec/lib/gitlab/downtime_check_spec.rb b/spec/lib/gitlab/downtime_check_spec.rb index 42d895e548e..1f1e4e0216c 100644 --- a/spec/lib/gitlab/downtime_check_spec.rb +++ b/spec/lib/gitlab/downtime_check_spec.rb @@ -11,12 +11,12 @@ describe Gitlab::DowntimeCheck do context 'when a migration does not specify if downtime is required' do it 'raises RuntimeError' do - expect(subject).to receive(:class_for_migration_file). - with(path). - and_return(Class.new) + expect(subject).to receive(:class_for_migration_file) + .with(path) + .and_return(Class.new) - expect { subject.check([path]) }. - to raise_error(RuntimeError, /it requires downtime/) + expect { subject.check([path]) } + .to raise_error(RuntimeError, /it requires downtime/) end end @@ -25,12 +25,12 @@ describe Gitlab::DowntimeCheck do it 'raises RuntimeError' do stub_const('TestMigration::DOWNTIME', true) - expect(subject).to receive(:class_for_migration_file). - with(path). - and_return(TestMigration) + expect(subject).to receive(:class_for_migration_file) + .with(path) + .and_return(TestMigration) - expect { subject.check([path]) }. - to raise_error(RuntimeError, /no reason was given/) + expect { subject.check([path]) } + .to raise_error(RuntimeError, /no reason was given/) end end @@ -39,9 +39,9 @@ describe Gitlab::DowntimeCheck do stub_const('TestMigration::DOWNTIME', true) stub_const('TestMigration::DOWNTIME_REASON', 'foo') - expect(subject).to receive(:class_for_migration_file). - with(path). - and_return(TestMigration) + expect(subject).to receive(:class_for_migration_file) + .with(path) + .and_return(TestMigration) messages = subject.check([path]) @@ -65,9 +65,9 @@ describe Gitlab::DowntimeCheck do expect(subject).to receive(:require).with(path) - expect(subject).to receive(:class_for_migration_file). - with(path). - and_return(TestMigration) + expect(subject).to receive(:class_for_migration_file) + .with(path) + .and_return(TestMigration) expect(subject).to receive(:puts).with(an_instance_of(String)) diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 3f79eaf7afb..cd0309e248d 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -91,7 +91,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do end end - context 'when the note contains slash commands' do + context 'when the note contains quick actions' do let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") } context 'and current user cannot update noteable' do diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index 28698e89c33..2ea5e6460a3 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -20,8 +20,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "properly renders plaintext-only email" do - expect(test_parse_body(fixture_file("emails/plaintext_only.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/plaintext_only.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp ### reply from default mail client in Windows 8.1 Metro @@ -46,8 +46,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "handles multiple paragraphs" do - expect(test_parse_body(fixture_file("emails/paragraphs.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/paragraphs.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp Is there any reason the *old* candy can't be be kept in silos while the new candy is imported into *new* silos? @@ -61,8 +61,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "handles multiple paragraphs when parsing html" do - expect(test_parse_body(fixture_file("emails/html_paragraphs.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/html_paragraphs.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp Awesome! @@ -74,8 +74,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "handles newlines" do - expect(test_parse_body(fixture_file("emails/newlines.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/newlines.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp This is my reply. It is my best reply. @@ -85,8 +85,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "handles inline reply" do - expect(test_parse_body(fixture_file("emails/inline_reply.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/inline_reply.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp > techAPJ <https://meta.discourse.org/users/techapj> > November 28 @@ -132,8 +132,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "properly renders email reply from gmail web client" do - expect(test_parse_body(fixture_file("emails/gmail_web.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/gmail_web.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp ### This is a reply from standard GMail in Google Chrome. @@ -151,8 +151,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "properly renders email reply from iOS default mail client" do - expect(test_parse_body(fixture_file("emails/ios_default.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/ios_default.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp ### this is a reply from iOS default mail @@ -166,8 +166,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "properly renders email reply from Android 5 gmail client" do - expect(test_parse_body(fixture_file("emails/android_gmail.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/android_gmail.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp ### this is a reply from Android 5 gmail @@ -184,8 +184,8 @@ describe Gitlab::Email::ReplyParser, lib: true do end it "properly renders email reply from Windows 8.1 Metro default mail client" do - expect(test_parse_body(fixture_file("emails/windows_8_metro.eml"))). - to eq( + expect(test_parse_body(fixture_file("emails/windows_8_metro.eml"))) + .to eq( <<-BODY.strip_heredoc.chomp ### reply from default mail client in Windows 8.1 Metro @@ -208,5 +208,9 @@ describe Gitlab::Email::ReplyParser, lib: true do it "properly renders html-only email from MS Outlook" do expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010") end + + it "does not wrap links with no href in unnecessary brackets" do + expect(test_parse_body(fixture_file("emails/html_empty_link.eml"))).to eq("no brackets!") + end end end diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 3c6ef7c7ccb..4a54d641b4e 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -15,13 +15,13 @@ describe Gitlab::EtagCaching::Middleware do end it 'does not add ETag header' do - _, headers, _ = middleware.call(build_env(path, if_none_match)) + _, headers, _ = middleware.call(build_request(path, if_none_match)) expect(headers['ETag']).to be_nil end it 'passes status code from app' do - status, _, _ = middleware.call(build_env(path, if_none_match)) + status, _, _ = middleware.call(build_request(path, if_none_match)) expect(status).to eq app_status_code end @@ -39,7 +39,7 @@ describe Gitlab::EtagCaching::Middleware do expect_any_instance_of(Gitlab::EtagCaching::Store) .to receive(:touch).and_return('123') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end context 'when If-None-Match header was specified' do @@ -51,7 +51,7 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_key_not_found, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end end end @@ -65,7 +65,7 @@ describe Gitlab::EtagCaching::Middleware do end it 'returns this value as header' do - _, headers, _ = middleware.call(build_env(path, if_none_match)) + _, headers, _ = middleware.call(build_request(path, if_none_match)) expect(headers['ETag']).to eq 'W/"123"' end @@ -82,17 +82,17 @@ describe Gitlab::EtagCaching::Middleware do it 'does not call app' do expect(app).not_to receive(:call) - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end it 'returns status code 304' do - status, _, _ = middleware.call(build_env(path, if_none_match)) + status, _, _ = middleware.call(build_request(path, if_none_match)) expect(status).to eq 304 end it 'returns empty body' do - _, _, body = middleware.call(build_env(path, if_none_match)) + _, _, body = middleware.call(build_request(path, if_none_match)) expect(body).to be_empty end @@ -103,17 +103,17 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_cache_hit, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end context 'when polling is disabled' do before do - allow(Gitlab::PollingInterval).to receive(:polling_enabled?). - and_return(false) + allow(Gitlab::PollingInterval).to receive(:polling_enabled?) + .and_return(false) end it 'returns status code 429' do - status, _, _ = middleware.call(build_env(path, if_none_match)) + status, _, _ = middleware.call(build_request(path, if_none_match)) expect(status).to eq 429 end @@ -131,7 +131,7 @@ describe Gitlab::EtagCaching::Middleware do it 'calls app' do expect(app).to receive(:call).and_return([app_status_code, {}, ['body']]) - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end it 'tracks "etag_caching_resource_changed" event' do @@ -142,7 +142,7 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_resource_changed, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end end @@ -160,7 +160,7 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_header_missing, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end end @@ -192,10 +192,7 @@ describe Gitlab::EtagCaching::Middleware do .to receive(:get).and_return(value) end - def build_env(path, if_none_match) - { - 'PATH_INFO' => path, - 'HTTP_IF_NONE_MATCH' => if_none_match - } + def build_request(path, if_none_match) + { 'PATH_INFO' => path, 'HTTP_IF_NONE_MATCH' => if_none_match } end end diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 2bb40827fcf..f69cb502ca6 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -2,115 +2,91 @@ require 'spec_helper' describe Gitlab::EtagCaching::Router do it 'matches issue notes endpoint' do - request = build_request( + result = described_class.match( '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'issue_notes' end it 'matches issue title endpoint' do - request = build_request( + result = described_class.match( '/my-group/my-project/issues/123/realtime_changes' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'issue_title' end it 'matches project pipelines endpoint' do - request = build_request( + result = described_class.match( '/my-group/my-project/pipelines.json' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'project_pipelines' end it 'matches commit pipelines endpoint' do - request = build_request( + result = described_class.match( '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'commit_pipelines' end it 'matches new merge request pipelines endpoint' do - request = build_request( + result = described_class.match( '/my-group/my-project/merge_requests/new.json' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'new_merge_request_pipelines' end it 'matches merge request pipelines endpoint' do - request = build_request( + result = described_class.match( '/my-group/my-project/merge_requests/234/pipelines.json' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'merge_request_pipelines' end it 'matches build endpoint' do - request = build_request( + result = described_class.match( '/my-group/my-project/builds/234.json' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'project_build' end it 'does not match blob with confusing name' do - request = build_request( + result = described_class.match( '/my-group/my-project/blob/master/pipelines.json' ) - result = described_class.match(request) - expect(result).to be_blank end it 'matches the environments path' do - request = build_request( + result = described_class.match( '/my-group/my-project/environments.json' ) - result = described_class.match(request) expect(result).to be_present - expect(result.name).to eq 'environments' end it 'matches pipeline#show endpoint' do - request = build_request( + result = described_class.match( '/my-group/my-project/pipelines/2.json' ) - result = described_class.match(request) - expect(result).to be_present expect(result.name).to eq 'project_pipeline' end - - def build_request(path) - double(path_info: path) - end end diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index a366d68a146..81bbd70ffb8 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -19,6 +19,19 @@ describe Gitlab::ExclusiveLease, type: :redis do end end + describe '#renew' do + it 'returns true when we have the existing lease' do + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to be_present + expect(lease.renew).to be_truthy + end + + it 'returns false when we dont have a lease' do + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.renew).to be_falsey + end + end + describe '#exists?' do it 'returns true for an existing lease' do lease = described_class.new(unique_key, timeout: 3600) diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb new file mode 100644 index 00000000000..b793176d84a --- /dev/null +++ b/spec/lib/gitlab/fake_application_settings_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::FakeApplicationSettings do + let(:defaults) { { signin_enabled: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } } + + subject { described_class.new(defaults) } + + it 'wraps OpenStruct variables properly' do + expect(subject.signin_enabled).to be_falsey + expect(subject.signup_enabled).to be_truthy + expect(subject.foobar).to eq('asdf') + end + + it 'defines predicate methods' do + expect(subject.signin_enabled?).to be_falsey + expect(subject.signup_enabled?).to be_truthy + end + + it 'predicate method changes when value is updated' do + subject.signin_enabled = true + + expect(subject.signin_enabled?).to be_truthy + end + + it 'does not define a predicate method' do + expect(subject.foobar?).to be_nil + end + + it 'does not override an existing predicate method' do + expect(subject.test?).to eq(123) + end +end diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index e5ba13bbaf8..695fd6f8573 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe Gitlab::FileDetector do describe '.types_in_paths' do it 'returns the file types for the given paths' do - expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION))). - to eq(%i{readme changelog version}) + expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION))) + .to eq(%i{readme changelog version}) end it 'does not include unrecognized file paths' do - expect(described_class.types_in_paths(%w(README.md foo.txt))). - to eq(%i{readme}) + expect(described_class.types_in_paths(%w(README.md foo.txt))) + .to eq(%i{readme}) end end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 5d416c9eec3..eaec699ad90 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -6,7 +6,9 @@ describe Gitlab::Gfm::ReferenceRewriter do let(:new_project) { create(:empty_project, name: 'new-project') } let(:user) { create(:user) } - before { old_project.team << [user, :reporter] } + before do + old_project.team << [user, :reporter] + end describe '#rewrite' do subject do diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_spec.rb index 1cfd8db09a5..b715fc3410a 100644 --- a/spec/lib/gitlab/git/attributes_spec.rb +++ b/spec/lib/gitlab/git/attributes_spec.rb @@ -14,13 +14,13 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'returns a Hash containing multiple attributes' do - expect(subject.attributes('test.sh')). - to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' }) + expect(subject.attributes('test.sh')) + .to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' }) end it 'returns a Hash containing attributes for a file with multiple extensions' do - expect(subject.attributes('test.haml.html')). - to eq({ 'gitlab-language' => 'haml' }) + expect(subject.attributes('test.haml.html')) + .to eq({ 'gitlab-language' => 'haml' }) end it 'returns a Hash containing attributes for a file in a directory' do @@ -28,8 +28,8 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'returns a Hash containing attributes with query string parameters' do - expect(subject.attributes('foo.cgi')). - to eq({ 'key' => 'value?p1=v1&p2=v2' }) + expect(subject.attributes('foo.cgi')) + .to eq({ 'key' => 'value?p1=v1&p2=v2' }) end it 'returns a Hash containing the attributes for an absolute path' do @@ -39,11 +39,11 @@ describe Gitlab::Git::Attributes, seed_helper: true do it 'returns a Hash containing the attributes when a pattern is defined using an absolute path' do # When a path is given without a leading slash it should still match # patterns defined with a leading slash. - expect(subject.attributes('foo.png')). - to eq({ 'gitlab-language' => 'png' }) + expect(subject.attributes('foo.png')) + .to eq({ 'gitlab-language' => 'png' }) - expect(subject.attributes('/foo.png')). - to eq({ 'gitlab-language' => 'png' }) + expect(subject.attributes('/foo.png')) + .to eq({ 'gitlab-language' => 'png' }) end it 'returns an empty Hash for a defined path without attributes' do @@ -74,8 +74,8 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'parses an entry that uses a tab to separate the pattern and attributes' do - expect(subject.patterns[File.join(path, '*.md')]). - to eq({ 'gitlab-language' => 'markdown' }) + expect(subject.patterns[File.join(path, '*.md')]) + .to eq({ 'gitlab-language' => 'markdown' }) end it 'stores patterns in reverse order' do @@ -91,9 +91,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'does not parse anything when the attributes file does not exist' do - expect(File).to receive(:exist?). - with(File.join(path, 'info/attributes')). - and_return(false) + expect(File).to receive(:exist?) + .with(File.join(path, 'info/attributes')) + .and_return(false) expect(subject.patterns).to eq({}) end @@ -115,13 +115,13 @@ describe Gitlab::Git::Attributes, seed_helper: true do it 'parses multiple attributes' do input = 'boolean key=value -negated' - expect(subject.parse_attributes(input)). - to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false }) + expect(subject.parse_attributes(input)) + .to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false }) end it 'parses attributes with query string parameters' do - expect(subject.parse_attributes('foo=bar?baz=1')). - to eq({ 'foo' => 'bar?baz=1' }) + expect(subject.parse_attributes('foo=bar?baz=1')) + .to eq({ 'foo' => 'bar?baz=1' }) end end @@ -133,9 +133,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'does not yield when the attributes file does not exist' do - expect(File).to receive(:exist?). - with(File.join(path, 'info/attributes')). - and_return(false) + expect(File).to receive(:exist?) + .with(File.join(path, 'info/attributes')) + .and_return(false) expect { |b| subject.each_line(&b) }.not_to yield_control end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index e6a07a58d73..58d3ee6b488 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe '.find' do + shared_examples 'finding blobs' do context 'file in subdir' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } @@ -92,15 +92,25 @@ describe Gitlab::Git::Blob, seed_helper: true do end it 'marks the blob as binary' do - expect(Gitlab::Git::Blob).to receive(:new). - with(hash_including(binary: true)). - and_call_original + expect(Gitlab::Git::Blob).to receive(:new) + .with(hash_including(binary: true)) + .and_call_original expect(blob).to be_binary end end end + describe '.find' do + context 'when project_raw_show Gitaly feature is enabled' do + it_behaves_like 'finding blobs' + end + + context 'when project_raw_show Gitaly feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding blobs' + end + end + describe '.raw' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 9eac7660cd1..9dba4397e79 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -45,8 +45,8 @@ describe Gitlab::Git::Branch, seed_helper: true do let(:branch) { described_class.new(repository, 'foo', gitaly_branch) } it 'parses Gitaly::FindLocalBranchResponse correctly' do - expect(Gitlab::Git::Commit).to receive(:decorate). - with(hash_including(attributes)).and_call_original + expect(Gitlab::Git::Commit).to receive(:decorate) + .with(hash_including(attributes)).and_call_original expect(branch.dereferenced_target.message.encoding).to be(Encoding::UTF_8) end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 3e44c577643..f20a14155dc 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -244,6 +244,33 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe '.find_all' do + it 'should return a return a collection of commits' do + commits = described_class.find_all(repository) + + expect(commits).not_to be_empty + expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) ) + end + + context 'while applying a sort order based on the `order` option' do + it "allows ordering topologically (no parents shown before their children)" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO) + + described_class.find_all(repository, order: :topo) + end + + it "allows ordering by date" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO) + + described_class.find_all(repository, order: :date) + end + + it "applies no sorting by default" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE) + + described_class.find_all(repository) + end + end + context 'max_count' do subject do commits = Gitlab::Git::Commit.find_all( @@ -281,26 +308,6 @@ describe Gitlab::Git::Commit, seed_helper: true do it { is_expected.to include(SeedRepo::FirstCommit::ID) } it { is_expected.not_to include(SeedRepo::LastCommit::ID) } end - - context 'contains feature + max_count' do - subject do - commits = Gitlab::Git::Commit.find_all( - repository, - contains: 'feature', - max_count: 7 - ) - - commits.map { |c| c.id } - end - - it 'has 7 elements' do - expect(subject.size).to eq(7) - end - - it { is_expected.not_to include(SeedRepo::Commit::PARENT_ID) } - it { is_expected.not_to include(SeedRepo::Commit::ID) } - it { is_expected.to include(SeedRepo::BigCommit::ID) } - end end end diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index a9a7bba2c05..d20298fa139 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -325,8 +325,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end it 'yields Diff instances even when they are too large' do - expect { |b| collection.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| collection.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'prunes diffs that are too large' do @@ -348,8 +348,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do let(:expanded) { true } it 'yields Diff instances even when they are quite big' do - expect { |b| subject.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| subject.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'does not prune diffs' do @@ -367,8 +367,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do let(:expanded) { false } it 'yields Diff instances even when they are quite big' do - expect { |b| subject.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| subject.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'prunes diffs that are quite big' do @@ -454,8 +454,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do let(:limits) { false } it 'yields Diff instances even when they are quite big' do - expect { |b| subject.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| subject.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'does not prune diffs' do diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index da213f617cc..d50ccb0df30 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -90,7 +90,7 @@ EOT let(:diff) { described_class.new(@rugged_diff) } it 'initializes the diff' do - expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil)) + expect(diff.to_hash).to eq(@raw_diff_hash) end it 'does not prune the diff' do @@ -100,8 +100,8 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - expect_any_instance_of(String).to receive(:bytesize). - and_return(1024 * 1024 * 1024) + expect_any_instance_of(String).to receive(:bytesize) + .and_return(1024 * 1024 * 1024) diff = described_class.new(@rugged_diff) @@ -130,8 +130,8 @@ EOT context 'using a large binary diff' do it 'does not prune the diff' do - expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?). - and_return(true) + expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?) + .and_return(true) diff = described_class.new(@rugged_diff) @@ -175,6 +175,14 @@ EOT expect(diff).to be_too_large end end + + context 'when the patch passed is not UTF-8-encoded' do + let(:raw_patch) { @raw_diff_hash[:diff].encode(Encoding::ASCII_8BIT) } + + it 'encodes diff patch to UTF-8' do + expect(diff.diff.encoding).to eq(Encoding::UTF_8) + end + end end end diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb new file mode 100644 index 00000000000..143aa2218c9 --- /dev/null +++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Git::GitmodulesParser do + it 'should parse a .gitmodules file correctly' do + parser = described_class.new(<<-'GITMODULES'.strip_heredoc) + [submodule "vendor/libgit2"] + path = vendor/libgit2 + [submodule "vendor/libgit2"] + url = https://github.com/nodegit/libgit2.git + + # a comment + [submodule "moved"] + path = new/path + url = https://example.com/some/project + [submodule "bogus"] + url = https://example.com/another/project + GITMODULES + + modules = parser.parse + + expect(modules).to eq({ + 'vendor/libgit2' => { 'name' => 'vendor/libgit2', + 'url' => 'https://github.com/nodegit/libgit2.git' }, + 'new/path' => { 'name' => 'moved', + 'url' => 'https://example.com/some/project' } + }) + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e1e4aa9fde9..ee25aeefa95 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -16,7 +16,9 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#root_ref' do context 'with gitaly disabled' do - before { allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) } + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + end it 'calls #discover_default_branch' do expect(repository).to receive(:discover_default_branch) @@ -25,8 +27,13 @@ describe Gitlab::Git::Repository, seed_helper: true do end context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the branch name from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) @@ -34,14 +41,14 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). - and_raise(GRPC::NotFound) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + .and_raise(GRPC::NotFound) expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository) end it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). - and_raise(GRPC::Unknown) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + .and_raise(GRPC::Unknown) expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) end end @@ -120,8 +127,13 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.not_to include("branch-from-space") } context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the branch names from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) @@ -129,14 +141,14 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). - and_raise(GRPC::NotFound) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + .and_raise(GRPC::NotFound) expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) end it 'wraps GRPC other exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). - and_raise(GRPC::Unknown) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + .and_raise(GRPC::Unknown) expect { subject }.to raise_error(Gitlab::Git::CommandError) end end @@ -158,8 +170,13 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.not_to include("v5.0.0") } context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the tag names from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) @@ -167,14 +184,14 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). - and_raise(GRPC::NotFound) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + .and_raise(GRPC::NotFound) expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) end it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). - and_raise(GRPC::Unknown) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + .and_raise(GRPC::Unknown) expect { subject }.to raise_error(Gitlab::Git::CommandError) end end @@ -331,7 +348,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } context 'where repo has submodules' do - let(:submodules) { repository.submodules('master') } + let(:submodules) { repository.send(:submodules, 'master') } let(:submodule) { submodules.first } it { expect(submodules).to be_kind_of Hash } @@ -341,7 +358,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(submodule).to eq([ "six", { "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", - "path" => "six", + "name" => "six", "url" => "git://github.com/randx/six.git" } ]) @@ -349,14 +366,14 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'should handle nested submodules correctly' do nested = submodules['nested/six'] - expect(nested['path']).to eq('nested/six') + expect(nested['name']).to eq('nested/six') expect(nested['url']).to eq('git://github.com/randx/six.git') expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') end it 'should handle deeply nested submodules correctly' do nested = submodules['deeper/nested/six'] - expect(nested['path']).to eq('deeper/nested/six') + expect(nested['name']).to eq('deeper/nested/six') expect(nested['url']).to eq('git://github.com/randx/six.git') expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') end @@ -366,17 +383,17 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'should not have an entry for an uncommited submodule dir' do - submodules = repository.submodules('fix-existing-submodule-dir') + submodules = repository.send(:submodules, 'fix-existing-submodule-dir') expect(submodules).not_to have_key('submodule-existing-dir') end it 'should handle tags correctly' do - submodules = repository.submodules('v1.2.1') + submodules = repository.send(:submodules, 'v1.2.1') expect(submodules.first).to eq([ "six", { "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", - "path" => "six", + "name" => "six", "url" => "git://github.com/randx/six.git" } ]) @@ -397,7 +414,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end context 'where repo doesn\'t have submodules' do - let(:submodules) { repository.submodules('6d39438') } + let(:submodules) { repository.send(:submodules, '6d39438') } it 'should return an empty hash' do expect(submodules).to be_empty end @@ -455,8 +472,8 @@ describe Gitlab::Git::Repository, seed_helper: true do end it "should move the tip of the master branch to the correct commit" do - new_tip = @normal_repo.rugged.references["refs/heads/master"]. - target.oid + new_tip = @normal_repo.rugged.references["refs/heads/master"] + .target.oid expect(new_tip).to eq(reset_commit) end @@ -1084,35 +1101,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#find_commits' do - it 'should return a return a collection of commits' do - commits = repository.find_commits - - expect(commits).not_to be_empty - expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) ) - end - - context 'while applying a sort order based on the `order` option' do - it "allows ordering topologically (no parents shown before their children)" do - expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO) - - repository.find_commits(order: :topo) - end - - it "allows ordering by date" do - expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO) - - repository.find_commits(order: :date) - end - - it "applies no sorting by default" do - expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE) - - repository.find_commits - end - end - end - describe '#branches with deleted branch' do before(:each) do ref = double() @@ -1280,24 +1268,29 @@ describe Gitlab::Git::Repository, seed_helper: true do end context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the branches from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). - and_return([]) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_return([]) @repo.local_branches end it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). - and_raise(GRPC::NotFound) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_raise(GRPC::NotFound) expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository) end it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). - and_raise(GRPC::Unknown) + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_raise(GRPC::Unknown) expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError) end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 36d1d777583..9a86cfa66e4 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' describe Gitlab::GitAccess, lib: true do let(:pull_access_check) { access.check('git-upload-pack', '_any') } let(:push_access_check) { access.check('git-receive-pack', '_any') } - let(:access) { Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: authentication_abilities) } + let(:access) { Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:actor) { user } let(:protocol) { 'ssh' } + let(:redirected_path) { nil } let(:authentication_abilities) do [ :read_project, @@ -60,7 +61,9 @@ describe Gitlab::GitAccess, lib: true do let(:actor) { deploy_key } context 'when the DeployKey has access to the project' do - before { deploy_key.projects << project } + before do + deploy_key.projects << project + end it 'allows pull access' do expect { pull_access_check }.not_to raise_error @@ -84,7 +87,9 @@ describe Gitlab::GitAccess, lib: true do context 'when actor is a User' do context 'when the User can read the project' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'allows pull access' do expect { pull_access_check }.not_to raise_error @@ -158,8 +163,50 @@ describe Gitlab::GitAccess, lib: true do end end + describe '#check_project_moved!' do + before do + project.team << [user, :master] + end + + context 'when a redirect was not followed to find the project' do + context 'pull code' do + it { expect { pull_access_check }.not_to raise_error } + end + + context 'push code' do + it { expect { push_access_check }.not_to raise_error } + end + end + + context 'when a redirect was followed to find the project' do + let(:redirected_path) { 'some/other-path' } + + context 'pull code' do + it { expect { pull_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) } + it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) } + + context 'http protocol' do + let(:protocol) { 'http' } + it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) } + end + end + + context 'push code' do + it { expect { push_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) } + it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) } + + context 'http protocol' do + let(:protocol) { 'http' } + it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) } + end + end + end + end + describe '#check_command_disabled!' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end context 'over http' do let(:protocol) { 'http' } @@ -196,7 +243,9 @@ describe Gitlab::GitAccess, lib: true do describe '#check_download_access!' do describe 'master permissions' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end context 'pull code' do it { expect { pull_access_check }.not_to raise_error } @@ -204,7 +253,9 @@ describe Gitlab::GitAccess, lib: true do end describe 'guest permissions' do - before { project.team << [user, :guest] } + before do + project.team << [user, :guest] + end context 'pull code' do it { expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.') } @@ -253,7 +304,9 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do context 'when project is authorized' do - before { key.projects << project } + before do + key.projects << project + end it { expect { pull_access_check }.not_to raise_error } end @@ -292,7 +345,9 @@ describe Gitlab::GitAccess, lib: true do end describe 'reporter user' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end context 'pull code' do it { expect { pull_access_check }.not_to raise_error } @@ -303,7 +358,9 @@ describe Gitlab::GitAccess, lib: true do let(:user) { create(:admin) } context 'when member of the project' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end context 'pull code' do it { expect { pull_access_check }.not_to raise_error } @@ -328,7 +385,9 @@ describe Gitlab::GitAccess, lib: true do end describe '#check_push_access!' do - before { merge_into_protected_branch } + before do + merge_into_protected_branch + end let(:unprotected_branch) { 'unprotected_branch' } let(:changes) do @@ -457,19 +516,25 @@ describe Gitlab::GitAccess, lib: true do [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type| context do - before { create(:protected_branch, name: protected_branch_name, project: project) } + before do + create(:protected_branch, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix) end context "when developers are allowed to push into the #{protected_branch_type} protected branch" do - before { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end context "developers are allowed to merge into the #{protected_branch_type} protected branch" do - before { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) + end context "when a merge request exists for the given source/target branch" do context "when the merge request is in progress" do @@ -496,13 +561,17 @@ describe Gitlab::GitAccess, lib: true do end context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do - before { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end context "when no one is allowed to push to the #{protected_branch_name} protected branch" do - before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, @@ -515,7 +584,9 @@ describe Gitlab::GitAccess, lib: true do let(:authentication_abilities) { build_authentication_abilities } context 'when project is authorized' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') } end @@ -549,7 +620,9 @@ describe Gitlab::GitAccess, lib: true do let(:can_push) { true } context 'when project is authorized' do - before { key.projects << project } + before do + key.projects << project + end it { expect { push_access_check }.not_to raise_error } end @@ -579,7 +652,9 @@ describe Gitlab::GitAccess, lib: true do let(:can_push) { false } context 'when project is authorized' do - before { key.projects << project } + before do + key.projects << project + end it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') } end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index a1eb95750ba..797ec8cb23e 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -1,9 +1,10 @@ require 'spec_helper' describe Gitlab::GitAccessWiki, lib: true do - let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) } + let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } + let(:redirected_path) { nil } let(:authentication_abilities) do [ :read_project, diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb index cf1bc74779e..dff5b25c712 100644 --- a/spec/lib/gitlab/gitaly_client/commit_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) described_class.new(repository).diff_from_parent(commit) end @@ -31,7 +31,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: initial_commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) described_class.new(repository).diff_from_parent(initial_commit) end @@ -61,7 +61,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([]) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request, kind_of(Hash)).and_return([]) described_class.new(repository).commit_deltas(commit) end @@ -76,7 +76,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: initial_commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([]) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request, kind_of(Hash)).and_return([]) described_class.new(repository).commit_deltas(initial_commit) end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb index b87dacb175b..7404ffe0f06 100644 --- a/spec/lib/gitlab/gitaly_client/notifications_spec.rb +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -3,12 +3,13 @@ require 'spec_helper' describe Gitlab::GitalyClient::Notifications do describe '#post_receive' do let(:project) { create(:empty_project) } - let(:repo_path) { project.repository.path_to_repo } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.path_with_namespace + '.git' } subject { described_class.new(project.repository) } it 'sends a post_receive message' do - expect_any_instance_of(Gitaly::Notifications::Stub). - to receive(:post_receive).with(gitaly_request_with_repo_path(repo_path)) + expect_any_instance_of(Gitaly::Notifications::Stub) + .to receive(:post_receive).with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) subject.post_receive end diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index d8cd2dcbd2a..42dba2ff874 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::GitalyClient::Ref do let(:project) { create(:empty_project) } - let(:repo_path) { project.repository.path_to_repo } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.path_with_namespace + '.git' } let(:client) { described_class.new(project.repository) } before do @@ -18,9 +19,10 @@ describe Gitlab::GitalyClient::Ref do describe '#branch_names' do it 'sends a find_all_branch_names message' do - expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_all_branch_names).with(gitaly_request_with_repo_path(repo_path)). - and_return([]) + expect_any_instance_of(Gitaly::Ref::Stub) + .to receive(:find_all_branch_names) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([]) client.branch_names end @@ -28,9 +30,10 @@ describe Gitlab::GitalyClient::Ref do describe '#tag_names' do it 'sends a find_all_tag_names message' do - expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_all_tag_names).with(gitaly_request_with_repo_path(repo_path)). - and_return([]) + expect_any_instance_of(Gitaly::Ref::Stub) + .to receive(:find_all_tag_names) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([]) client.tag_names end @@ -38,9 +41,10 @@ describe Gitlab::GitalyClient::Ref do describe '#default_branch_name' do it 'sends a find_default_branch_name message' do - expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_default_branch_name).with(gitaly_request_with_repo_path(repo_path)). - and_return(double(name: 'foo')) + expect_any_instance_of(Gitaly::Ref::Stub) + .to receive(:find_default_branch_name) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(name: 'foo')) client.default_branch_name end @@ -48,18 +52,19 @@ describe Gitlab::GitalyClient::Ref do describe '#local_branches' do it 'sends a find_local_branches message' do - expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_local_branches).with(gitaly_request_with_repo_path(repo_path)). - and_return([]) + expect_any_instance_of(Gitaly::Ref::Stub) + .to receive(:find_local_branches) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([]) client.local_branches end it 'parses and sends the sort parameter' do - expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_local_branches). - with(gitaly_request_with_params(sort_by: :UPDATED_DESC)). - and_return([]) + expect_any_instance_of(Gitaly::Ref::Stub) + .to receive(:find_local_branches) + .with(gitaly_request_with_params(sort_by: :UPDATED_DESC), kind_of(Hash)) + .and_return([]) client.local_branches(sort_by: 'updated_desc') end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 95ecba67532..ce7b18b784a 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -5,7 +5,9 @@ require 'spec_helper' describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do describe '.stub' do # Notice that this is referring to gRPC "stubs", not rspec stubs - before { described_class.clear_stubs! } + before do + described_class.clear_stubs! + end context 'when passed a UNIX socket address' do it 'passes the address as-is to GRPC' do @@ -41,7 +43,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do let(:real_feature_name) { "gitaly_#{feature_name}" } context 'when Gitaly is disabled' do - before { allow(described_class).to receive(:enabled?).and_return(false) } + before do + allow(described_class).to receive(:enabled?).and_return(false) + end it 'returns false' do expect(described_class.feature_enabled?(feature_name)).to be(false) @@ -66,7 +70,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to disable" do - before { Feature.get(real_feature_name).disable } + before do + Feature.get(real_feature_name).disable + end it 'returns false' do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) @@ -74,7 +80,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to enable" do - before { Feature.get(real_feature_name).enable } + before do + Feature.get(real_feature_name).enable + end it 'returns true' do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) @@ -82,7 +90,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to a percentage of time" do - before { Feature.get(real_feature_name).enable_percentage_of_time(70) } + before do + Feature.get(real_feature_name).enable_percentage_of_time(70) + end it 'bases the result on pseudo-random numbers' do expect(Random).to receive(:rand).and_return(0.3) @@ -104,7 +114,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to disable" do - before { Feature.get(real_feature_name).disable } + before do + Feature.get(real_feature_name).disable + end it 'returns false' do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb index 9b499b593d3..4f588da0a83 100644 --- a/spec/lib/gitlab/gitlab_import/importer_spec.rb +++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb @@ -45,8 +45,8 @@ describe Gitlab::GitlabImport::Importer, lib: true do def stub_request(path, body) url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100" - WebMock.stub_request(:get, url). - to_return( + WebMock.stub_request(:get, url) + .to_return( headers: { 'Content-Type' => 'application/json' }, body: body ) diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 5d0ed1522b3..08010c2d0e2 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -17,6 +17,12 @@ describe Gitlab::GroupHierarchy, :postgresql do it 'includes all of the ancestors' do expect(relation).to include(parent, child1) end + + it 'uses ancestors_base #initialize argument' do + relation = described_class.new(Group.where(id: child2.id), Group.none).base_and_ancestors + + expect(relation).to include(parent, child1, child2) + end end describe '#base_and_descendants' do @@ -31,6 +37,12 @@ describe Gitlab::GroupHierarchy, :postgresql do it 'includes all the descendants' do expect(relation).to include(child1, child2) end + + it 'uses descendants_base #initialize argument' do + relation = described_class.new(Group.none, Group.where(id: parent.id)).base_and_descendants + + expect(relation).to include(parent, child1, child2) + end end describe '#all_groups' do @@ -49,5 +61,17 @@ describe Gitlab::GroupHierarchy, :postgresql do it 'includes the descendants' do expect(relation).to include(child2) end + + it 'uses ancestors_base #initialize argument for ancestors' do + relation = described_class.new(Group.where(id: child1.id), Group.where(id: Group.maximum(:id).succ)).all_groups + + expect(relation).to include(parent) + end + + it 'uses descendants_base #initialize argument for descendants' do + relation = described_class.new(Group.where(id: Group.maximum(:id).succ), Group.where(id: child1.id)).all_groups + + expect(relation).to include(child2) + end end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index c2bb9f9a166..07687b470c5 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -15,7 +15,9 @@ describe Gitlab::Highlight, lib: true do Gitlab::Highlight.new(blob.path, blob.data, repository: repository) end - before { project.change_head('gitattributes') } + before do + project.change_head('gitattributes') + end describe 'basic language selection' do let(:path) { 'custom-highlighting/test.gitlab-custom' } @@ -49,8 +51,8 @@ describe Gitlab::Highlight, lib: true do end it 'links dependencies via DependencyLinker' do - expect(Gitlab::DependencyLinker).to receive(:link). - with('file.name', 'Contents', anything).and_call_original + expect(Gitlab::DependencyLinker).to receive(:link) + .with('file.name', 'Contents', anything).and_call_original described_class.highlight('file.name', 'Contents') end diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb index a3dbeaa3753..0dba4132101 100644 --- a/spec/lib/gitlab/i18n_spec.rb +++ b/spec/lib/gitlab/i18n_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::I18n, lib: true do let(:user) { create(:user, preferred_language: 'es') } describe '.locale=' do - after { described_class.use_default_locale } + after do + described_class.use_default_locale + end it 'sets the locale based on current user preferred language' do described_class.locale = user.preferred_language diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index bb758a8a202..29912da2e25 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -12,8 +12,8 @@ describe Gitlab::Identifier do describe '#identify' do context 'without an identifier' do it 'identifies the user using a commit' do - expect(identifier).to receive(:identify_using_commit). - with(project, '123') + expect(identifier).to receive(:identify_using_commit) + .with(project, '123') identifier.identify('', project, '123') end @@ -21,8 +21,8 @@ describe Gitlab::Identifier do context 'with a user identifier' do it 'identifies the user using a user ID' do - expect(identifier).to receive(:identify_using_user). - with("user-#{user.id}") + expect(identifier).to receive(:identify_using_user) + .with("user-#{user.id}") identifier.identify("user-#{user.id}", project, '123') end @@ -30,8 +30,8 @@ describe Gitlab::Identifier do context 'with an SSH key identifier' do it 'identifies the user using an SSH key ID' do - expect(identifier).to receive(:identify_using_ssh_key). - with("key-#{key.id}") + expect(identifier).to receive(:identify_using_ssh_key) + .with("key-#{key.id}") identifier.identify("key-#{key.id}", project, '123') end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 412eb33b35b..a5f09f1856e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -88,6 +88,9 @@ merge_requests: - head_pipeline merge_request_diff: - merge_request +- merge_request_diff_files +merge_request_diff_files: +- merge_request_diff pipelines: - project - user diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index 42f3fc59f04..70796781532 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -44,6 +44,8 @@ describe 'forked project import', services: true do end it 'can access the MR' do - expect(project.merge_requests.first.ensure_ref_fetched.first).to include('refs/merge-requests/1/head') + project.merge_requests.first.ensure_ref_fetched + + expect(project.repository.ref_exists?('refs/merge-requests/1/head')).to be_truthy end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index e3599d6fe59..98c117b4cd8 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2821,9 +2821,11 @@ "committer_email": "dmitriy.zaporozhets@gmail.com" } ], - "utf8_st_diffs": [ + "merge_request_diff_files": [ { - "diff": "Binary files a/.DS_Store and /dev/null differ\n", + "merge_request_diff_id": 27, + "relative_order": 0, + "utf8_diff": "Binary files a/.DS_Store and /dev/null differ\n", "new_path": ".DS_Store", "old_path": ".DS_Store", "a_mode": "100644", @@ -2834,7 +2836,9 @@ "too_large": false }, { - "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", + "merge_request_diff_id": 27, + "relative_order": 1, + "utf8_diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n", "new_path": ".gitignore", "old_path": ".gitignore", "a_mode": "100644", @@ -2845,7 +2849,9 @@ "too_large": false }, { - "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", + "merge_request_diff_id": 27, + "relative_order": 2, + "utf8_diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n", "new_path": ".gitmodules", "old_path": ".gitmodules", "a_mode": "100644", @@ -2856,7 +2862,9 @@ "too_large": false }, { - "diff": "Binary files a/files/.DS_Store and /dev/null differ\n", + "merge_request_diff_id": 27, + "relative_order": 3, + "utf8_diff": "Binary files a/files/.DS_Store and /dev/null differ\n", "new_path": "files/.DS_Store", "old_path": "files/.DS_Store", "a_mode": "100644", @@ -2867,7 +2875,9 @@ "too_large": false }, { - "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n", + "merge_request_diff_id": 27, + "relative_order": 4, + "utf8_diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n", "new_path": "files/ruby/feature.rb", "old_path": "files/ruby/feature.rb", "a_mode": "0", @@ -2878,7 +2888,9 @@ "too_large": false }, { - "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "merge_request_diff_id": 27, + "relative_order": 5, + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -2889,7 +2901,9 @@ "too_large": false }, { - "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", + "merge_request_diff_id": 27, + "relative_order": 6, + "utf8_diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n", "new_path": "files/ruby/regex.rb", "old_path": "files/ruby/regex.rb", "a_mode": "100644", @@ -2900,7 +2914,9 @@ "too_large": false }, { - "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", + "merge_request_diff_id": 27, + "relative_order": 7, + "utf8_diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n", "new_path": "gitlab-grack", "old_path": "gitlab-grack", "a_mode": "0", @@ -2911,7 +2927,9 @@ "too_large": false }, { - "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", + "merge_request_diff_id": 27, + "relative_order": 8, + "utf8_diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n", "new_path": "gitlab-shell", "old_path": "gitlab-shell", "a_mode": "0", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 14338515892..c11b15a811b 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -86,8 +86,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do it 'has the correct data for merge request st_diffs' do # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ + # one MergeRequestDiff uses the new format, where st_diffs is expected to be nil - expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9) + expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(8) + end + + it 'has the correct data for merge request diff files' do + expect(MergeRequestDiffFile.where.not(diff: nil).count).to eq(9) end it 'has the correct time for merge request st_commits' do diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 5aeb29b7fec..e52f79513f1 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -83,6 +83,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests'].first['merge_request_diff']['utf8_st_diffs']).not_to be_nil end + it 'has merge request diff files' do + expect(saved_project_json['merge_requests'].first['merge_request_diff']['merge_request_diff_files']).not_to be_empty + end + it 'has merge requests comments' do expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty end @@ -145,6 +149,12 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(project_tree_saver.save).to be true end + it 'does not complain about non UTF-8 characters in MR diff files' do + ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") + + expect(project_tree_saver.save).to be true + end + context 'group members' do let(:user2) { create(:user, email: 'group@member.com') } let(:member_emails) do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 50ff6ecc1e0..fadd3ad1330 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -172,6 +172,17 @@ MergeRequestDiff: - real_size - head_commit_sha - start_commit_sha +MergeRequestDiffFile: +- merge_request_diff_id +- relative_order +- new_file +- renamed_file +- deleted_file +- new_path +- old_path +- a_mode +- b_mode +- too_large Ci::Pipeline: - id - project_id diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb index 780f5b1f8d7..6186cec2689 100644 --- a/spec/lib/gitlab/job_waiter_spec.rb +++ b/spec/lib/gitlab/job_waiter_spec.rb @@ -4,8 +4,8 @@ describe Gitlab::JobWaiter do describe '#wait' do let(:waiter) { described_class.new(%w(a)) } it 'returns when all jobs have been completed' do - expect(Gitlab::SidekiqStatus).to receive(:all_completed?).with(%w(a)). - and_return(true) + expect(Gitlab::SidekiqStatus).to receive(:all_completed?).with(%w(a)) + .and_return(true) expect(waiter).not_to receive(:sleep) @@ -13,9 +13,9 @@ describe Gitlab::JobWaiter do end it 'sleeps between checking the job statuses' do - expect(Gitlab::SidekiqStatus).to receive(:all_completed?). - with(%w(a)). - and_return(false, true) + expect(Gitlab::SidekiqStatus).to receive(:all_completed?) + .with(%w(a)) + .and_return(false, true) expect(waiter).to receive(:sleep).with(described_class::INTERVAL) diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index 91f9d06b85a..e8c599a95ee 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::Kubernetes do + include KubernetesHelpers include described_class describe '#container_exec_url' do @@ -36,4 +37,13 @@ describe Gitlab::Kubernetes do it { expect(result.query).to match(/\Acontainer=container\+1&/) } end end + + describe '#filter_by_label' do + it 'returns matching labels' do + matching_items = [kube_pod(app: 'foo')] + items = matching_items + [kube_pod] + + expect(filter_by_label(items, app: 'foo')).to eq(matching_items) + end + end end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 563c074017a..9454878b057 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -74,13 +74,17 @@ describe Gitlab::LDAP::Adapter, lib: true do subject { adapter.dn_matches_filter?(:dn, :filter) } context "when the search result is non-empty" do - before { allow(adapter).to receive(:ldap_search).and_return([:foo]) } + before do + allow(adapter).to receive(:ldap_search).and_return([:foo]) + end it { is_expected.to be_truthy } end context "when the search result is empty" do - before { allow(adapter).to receive(:ldap_search).and_return([]) } + before do + allow(adapter).to receive(:ldap_search).and_return([]) + end it { is_expected.to be_falsey } end @@ -91,13 +95,17 @@ describe Gitlab::LDAP::Adapter, lib: true do context "when the search is successful" do context "and the result is non-empty" do - before { allow(ldap).to receive(:search).and_return([:foo]) } + before do + allow(ldap).to receive(:search).and_return([:foo]) + end it { is_expected.to eq [:foo] } end context "and the result is empty" do - before { allow(ldap).to receive(:search).and_return([]) } + before do + allow(ldap).to receive(:search).and_return([]) + end it { is_expected.to eq [] } end diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb index b8f3290e84c..f689b47fec4 100644 --- a/spec/lib/gitlab/ldap/authentication_spec.rb +++ b/spec/lib/gitlab/ldap/authentication_spec.rb @@ -16,8 +16,8 @@ describe Gitlab::LDAP::Authentication, lib: true do # try only to fake the LDAP call adapter = double('adapter', dn: dn).as_null_object - allow_any_instance_of(described_class). - to receive(:adapter).and_return(adapter) + allow_any_instance_of(described_class) + .to receive(:adapter).and_return(adapter) expect(described_class.login(login, password)).to be_truthy end @@ -25,8 +25,8 @@ describe Gitlab::LDAP::Authentication, lib: true do it "is false if the user does not exist" do # try only to fake the LDAP call adapter = double('adapter', dn: dn).as_null_object - allow_any_instance_of(described_class). - to receive(:adapter).and_return(adapter) + allow_any_instance_of(described_class) + .to receive(:adapter).and_return(adapter) expect(described_class.login(login, password)).to be_falsey end @@ -36,8 +36,8 @@ describe Gitlab::LDAP::Authentication, lib: true do # try only to fake the LDAP call adapter = double('adapter', bind_as: nil).as_null_object - allow_any_instance_of(described_class). - to receive(:adapter).and_return(adapter) + allow_any_instance_of(described_class) + .to receive(:adapter).and_return(adapter) expect(described_class.login(login, password)).to be_falsey end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index a0eda685ca3..b796d8bf076 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -167,13 +167,15 @@ describe Gitlab::LDAP::User, lib: true do describe 'blocking' do def configure_block(value) - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:block_auto_created_users).and_return(value) + allow_any_instance_of(Gitlab::LDAP::Config) + .to receive(:block_auto_created_users).and_return(value) end context 'signup' do context 'dont block on create' do - before { configure_block(false) } + before do + configure_block(false) + end it do ldap_user.save @@ -183,7 +185,9 @@ describe Gitlab::LDAP::User, lib: true do end context 'block on create' do - before { configure_block(true) } + before do + configure_block(true) + end it do ldap_user.save @@ -200,7 +204,9 @@ describe Gitlab::LDAP::User, lib: true do end context 'dont block on create' do - before { configure_block(false) } + before do + configure_block(false) + end it do ldap_user.save @@ -210,7 +216,9 @@ describe Gitlab::LDAP::User, lib: true do end context 'block on create' do - before { configure_block(true) } + before do + configure_block(true) + end it do ldap_user.save diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index a986cb520fb..4b19ee19103 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -78,11 +78,11 @@ describe Gitlab::Metrics::Instrumentation do end it 'tracks the call duration upon calling the method' do - allow(Gitlab::Metrics).to receive(:method_call_threshold). - and_return(0) + allow(Gitlab::Metrics).to receive(:method_call_threshold) + .and_return(0) - allow(described_class).to receive(:transaction). - and_return(transaction) + allow(described_class).to receive(:transaction) + .and_return(transaction) expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @@ -90,8 +90,8 @@ describe Gitlab::Metrics::Instrumentation do end it 'does not track method calls below a given duration threshold' do - allow(Gitlab::Metrics).to receive(:method_call_threshold). - and_return(100) + allow(Gitlab::Metrics).to receive(:method_call_threshold) + .and_return(100) expect(transaction).not_to receive(:add_metric) @@ -137,8 +137,8 @@ describe Gitlab::Metrics::Instrumentation do before do allow(Gitlab::Metrics).to receive(:enabled?).and_return(true) - described_class. - instrument_instance_method(@dummy, :bar) + described_class + .instrument_instance_method(@dummy, :bar) end it 'instruments instances of the Class' do @@ -156,11 +156,11 @@ describe Gitlab::Metrics::Instrumentation do end it 'tracks the call duration upon calling the method' do - allow(Gitlab::Metrics).to receive(:method_call_threshold). - and_return(0) + allow(Gitlab::Metrics).to receive(:method_call_threshold) + .and_return(0) - allow(described_class).to receive(:transaction). - and_return(transaction) + allow(described_class).to receive(:transaction) + .and_return(transaction) expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @@ -168,8 +168,8 @@ describe Gitlab::Metrics::Instrumentation do end it 'does not track method calls below a given duration threshold' do - allow(Gitlab::Metrics).to receive(:method_call_threshold). - and_return(100) + allow(Gitlab::Metrics).to receive(:method_call_threshold) + .and_return(100) expect(transaction).not_to receive(:add_metric) @@ -183,8 +183,8 @@ describe Gitlab::Metrics::Instrumentation do end it 'does not instrument the method' do - described_class. - instrument_instance_method(@dummy, :bar) + described_class + .instrument_instance_method(@dummy, :bar) expect(described_class.instrumented?(@dummy)).to eq(false) end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index fb470ea7568..ec415f2bd85 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -26,8 +26,8 @@ describe Gitlab::Metrics::RackMiddleware do allow(app).to receive(:call).with(env) - expect(middleware).to receive(:tag_controller). - with(an_instance_of(Gitlab::Metrics::Transaction), env) + expect(middleware).to receive(:tag_controller) + .with(an_instance_of(Gitlab::Metrics::Transaction), env) middleware.call(env) end @@ -40,8 +40,8 @@ describe Gitlab::Metrics::RackMiddleware do allow(app).to receive(:call).with(env) - expect(middleware).to receive(:tag_endpoint). - with(an_instance_of(Gitlab::Metrics::Transaction), env) + expect(middleware).to receive(:tag_endpoint) + .with(an_instance_of(Gitlab::Metrics::Transaction), env) middleware.call(env) end @@ -49,8 +49,8 @@ describe Gitlab::Metrics::RackMiddleware do it 'tracks any raised exceptions' do expect(app).to receive(:call).with(env).and_raise(RuntimeError) - expect_any_instance_of(Gitlab::Metrics::Transaction). - to receive(:add_event).with(:rails_exception) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .to receive(:add_event).with(:rails_exception) expect { middleware.call(env) }.to raise_error(RuntimeError) end diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb index 1ab923b58cf..d07ce6f81af 100644 --- a/spec/lib/gitlab/metrics/sampler_spec.rb +++ b/spec/lib/gitlab/metrics/sampler_spec.rb @@ -38,8 +38,8 @@ describe Gitlab::Metrics::Sampler do describe '#flush' do it 'schedules the metrics using Sidekiq' do - expect(Gitlab::Metrics).to receive(:submit_metrics). - with([an_instance_of(Hash)]) + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([an_instance_of(Hash)]) sampler.sample_memory_usage sampler.flush @@ -48,12 +48,12 @@ describe Gitlab::Metrics::Sampler do describe '#sample_memory_usage' do it 'adds a metric containing the memory usage' do - expect(Gitlab::Metrics::System).to receive(:memory_usage). - and_return(9000) + expect(Gitlab::Metrics::System).to receive(:memory_usage) + .and_return(9000) - expect(sampler).to receive(:add_metric). - with(/memory_usage/, value: 9000). - and_call_original + expect(sampler).to receive(:add_metric) + .with(/memory_usage/, value: 9000) + .and_call_original sampler.sample_memory_usage end @@ -61,12 +61,12 @@ describe Gitlab::Metrics::Sampler do describe '#sample_file_descriptors' do it 'adds a metric containing the amount of open file descriptors' do - expect(Gitlab::Metrics::System).to receive(:file_descriptor_count). - and_return(4) + expect(Gitlab::Metrics::System).to receive(:file_descriptor_count) + .and_return(4) - expect(sampler).to receive(:add_metric). - with(/file_descriptors/, value: 4). - and_call_original + expect(sampler).to receive(:add_metric) + .with(/file_descriptors/, value: 4) + .and_call_original sampler.sample_file_descriptors end @@ -75,10 +75,10 @@ describe Gitlab::Metrics::Sampler do if Gitlab::Metrics.mri? describe '#sample_objects' do it 'adds a metric containing the amount of allocated objects' do - expect(sampler).to receive(:add_metric). - with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)). - at_least(:once). - and_call_original + expect(sampler).to receive(:add_metric) + .with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)) + .at_least(:once) + .and_call_original sampler.sample_objects end @@ -86,8 +86,8 @@ describe Gitlab::Metrics::Sampler do it 'ignores classes without a name' do expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 }) - expect(sampler).not_to receive(:add_metric). - with('object_counts', an_instance_of(Hash), type: nil) + expect(sampler).not_to receive(:add_metric) + .with('object_counts', an_instance_of(Hash), type: nil) sampler.sample_objects end @@ -98,9 +98,9 @@ describe Gitlab::Metrics::Sampler do it 'adds a metric containing garbage collection statistics' do expect(GC::Profiler).to receive(:total_time).and_return(0.24) - expect(sampler).to receive(:add_metric). - with(/gc_statistics/, an_instance_of(Hash)). - and_call_original + expect(sampler).to receive(:add_metric) + .with(/gc_statistics/, an_instance_of(Hash)) + .and_call_original sampler.sample_gc end @@ -110,9 +110,9 @@ describe Gitlab::Metrics::Sampler do it 'prefixes the series name for a Rails process' do expect(sampler).to receive(:sidekiq?).and_return(false) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('rails_cats', { value: 10 }, {}). - and_call_original + expect(Gitlab::Metrics::Metric).to receive(:new) + .with('rails_cats', { value: 10 }, {}) + .and_call_original sampler.add_metric('cats', value: 10) end @@ -120,9 +120,9 @@ describe Gitlab::Metrics::Sampler do it 'prefixes the series name for a Sidekiq process' do expect(sampler).to receive(:sidekiq?).and_return(true) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('sidekiq_cats', { value: 10 }, {}). - and_call_original + expect(Gitlab::Metrics::Metric).to receive(:new) + .with('sidekiq_cats', { value: 10 }, {}) + .and_call_original sampler.add_metric('cats', value: 10) end diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb index acaba785606..b576d7173f5 100644 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -8,12 +8,12 @@ describe Gitlab::Metrics::SidekiqMiddleware do it 'tracks the transaction' do worker = double(:worker, class: double(:class, name: 'TestWorker')) - expect(Gitlab::Metrics::Transaction).to receive(:new). - with('TestWorker#perform'). - and_call_original + expect(Gitlab::Metrics::Transaction).to receive(:new) + .with('TestWorker#perform') + .and_call_original - expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set). - with(:sidekiq_queue_duration, instance_of(Float)) + expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set) + .with(:sidekiq_queue_duration, instance_of(Float)) expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) @@ -23,12 +23,12 @@ describe Gitlab::Metrics::SidekiqMiddleware do it 'tracks the transaction (for messages without `enqueued_at`)' do worker = double(:worker, class: double(:class, name: 'TestWorker')) - expect(Gitlab::Metrics::Transaction).to receive(:new). - with('TestWorker#perform'). - and_call_original + expect(Gitlab::Metrics::Transaction).to receive(:new) + .with('TestWorker#perform') + .and_call_original - expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set). - with(:sidekiq_queue_duration, instance_of(Float)) + expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set) + .with(:sidekiq_queue_duration, instance_of(Float)) expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) @@ -38,17 +38,17 @@ describe Gitlab::Metrics::SidekiqMiddleware do it 'tracks any raised exceptions' do worker = double(:worker, class: double(:class, name: 'TestWorker')) - expect_any_instance_of(Gitlab::Metrics::Transaction). - to receive(:run).and_raise(RuntimeError) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .to receive(:run).and_raise(RuntimeError) - expect_any_instance_of(Gitlab::Metrics::Transaction). - to receive(:add_event).with(:sidekiq_exception) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .to receive(:add_event).with(:sidekiq_exception) - expect_any_instance_of(Gitlab::Metrics::Transaction). - to receive(:finish) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .to receive(:finish) - expect { middleware.call(worker, message, :test) }. - to raise_error(RuntimeError) + expect { middleware.call(worker, message, :test) } + .to raise_error(RuntimeError) end end end diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index 0695c5ce096..e7b595405a8 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -21,11 +21,11 @@ describe Gitlab::Metrics::Subscribers::ActionView do values = { duration: 2.1 } tags = { view: 'app/views/x.html.haml' } - expect(transaction).to receive(:increment). - with(:view_duration, 2.1) + expect(transaction).to receive(:increment) + .with(:view_duration, 2.1) - expect(transaction).to receive(:add_metric). - with(described_class::SERIES, values, tags) + expect(transaction).to receive(:add_metric) + .with(described_class::SERIES, values, tags) subscriber.render_template(event) end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 49699ffe28f..ce6587e993f 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -12,8 +12,8 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do describe '#sql' do describe 'without a current transaction' do it 'simply returns' do - expect_any_instance_of(Gitlab::Metrics::Transaction). - not_to receive(:increment) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .not_to receive(:increment) subscriber.sql(event) end @@ -21,15 +21,15 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do describe 'with a current transaction' do it 'increments the :sql_duration value' do - expect(subscriber).to receive(:current_transaction). - at_least(:once). - and_return(transaction) + expect(subscriber).to receive(:current_transaction) + .at_least(:once) + .and_return(transaction) - expect(transaction).to receive(:increment). - with(:sql_duration, 0.2) + expect(transaction).to receive(:increment) + .with(:sql_duration, 0.2) - expect(transaction).to receive(:increment). - with(:sql_count, 1) + expect(transaction).to receive(:increment) + .with(:sql_count, 1) subscriber.sql(event) end diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index d986c6fac43..f04dc8dcc02 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -8,26 +8,26 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_read' do it 'increments the cache_read duration' do - expect(subscriber).to receive(:increment). - with(:cache_read, event.duration) + expect(subscriber).to receive(:increment) + .with(:cache_read, event.duration) subscriber.cache_read(event) end context 'with a transaction' do before do - allow(subscriber).to receive(:current_transaction). - and_return(transaction) + allow(subscriber).to receive(:current_transaction) + .and_return(transaction) end context 'with hit event' do let(:event) { double(:event, duration: 15.2, payload: { hit: true }) } it 'increments the cache_read_hit count' do - expect(transaction).to receive(:increment). - with(:cache_read_hit_count, 1) - expect(transaction).to receive(:increment). - with(any_args).at_least(1) # Other calls + expect(transaction).to receive(:increment) + .with(:cache_read_hit_count, 1) + expect(transaction).to receive(:increment) + .with(any_args).at_least(1) # Other calls subscriber.cache_read(event) end @@ -36,8 +36,8 @@ describe Gitlab::Metrics::Subscribers::RailsCache do let(:event) { double(:event, duration: 15.2, payload: { hit: true, super_operation: :fetch }) } it 'does not increment cache read miss' do - expect(transaction).not_to receive(:increment). - with(:cache_read_hit_count, 1) + expect(transaction).not_to receive(:increment) + .with(:cache_read_hit_count, 1) subscriber.cache_read(event) end @@ -48,10 +48,10 @@ describe Gitlab::Metrics::Subscribers::RailsCache do let(:event) { double(:event, duration: 15.2, payload: { hit: false }) } it 'increments the cache_read_miss count' do - expect(transaction).to receive(:increment). - with(:cache_read_miss_count, 1) - expect(transaction).to receive(:increment). - with(any_args).at_least(1) # Other calls + expect(transaction).to receive(:increment) + .with(:cache_read_miss_count, 1) + expect(transaction).to receive(:increment) + .with(any_args).at_least(1) # Other calls subscriber.cache_read(event) end @@ -60,8 +60,8 @@ describe Gitlab::Metrics::Subscribers::RailsCache do let(:event) { double(:event, duration: 15.2, payload: { hit: false, super_operation: :fetch }) } it 'does not increment cache read miss' do - expect(transaction).not_to receive(:increment). - with(:cache_read_miss_count, 1) + expect(transaction).not_to receive(:increment) + .with(:cache_read_miss_count, 1) subscriber.cache_read(event) end @@ -72,8 +72,8 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_write' do it 'increments the cache_write duration' do - expect(subscriber).to receive(:increment). - with(:cache_write, event.duration) + expect(subscriber).to receive(:increment) + .with(:cache_write, event.duration) subscriber.cache_write(event) end @@ -81,8 +81,8 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_delete' do it 'increments the cache_delete duration' do - expect(subscriber).to receive(:increment). - with(:cache_delete, event.duration) + expect(subscriber).to receive(:increment) + .with(:cache_delete, event.duration) subscriber.cache_delete(event) end @@ -90,8 +90,8 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_exist?' do it 'increments the cache_exists duration' do - expect(subscriber).to receive(:increment). - with(:cache_exists, event.duration) + expect(subscriber).to receive(:increment) + .with(:cache_exists, event.duration) subscriber.cache_exist?(event) end @@ -108,13 +108,13 @@ describe Gitlab::Metrics::Subscribers::RailsCache do context 'with a transaction' do before do - allow(subscriber).to receive(:current_transaction). - and_return(transaction) + allow(subscriber).to receive(:current_transaction) + .and_return(transaction) end it 'increments the cache_read_hit count' do - expect(transaction).to receive(:increment). - with(:cache_read_hit_count, 1) + expect(transaction).to receive(:increment) + .with(:cache_read_hit_count, 1) subscriber.cache_fetch_hit(event) end @@ -132,13 +132,13 @@ describe Gitlab::Metrics::Subscribers::RailsCache do context 'with a transaction' do before do - allow(subscriber).to receive(:current_transaction). - and_return(transaction) + allow(subscriber).to receive(:current_transaction) + .and_return(transaction) end it 'increments the cache_fetch_miss count' do - expect(transaction).to receive(:increment). - with(:cache_read_miss_count, 1) + expect(transaction).to receive(:increment) + .with(:cache_read_miss_count, 1) subscriber.cache_generate(event) end @@ -156,22 +156,22 @@ describe Gitlab::Metrics::Subscribers::RailsCache do context 'with a transaction' do before do - allow(subscriber).to receive(:current_transaction). - and_return(transaction) + allow(subscriber).to receive(:current_transaction) + .and_return(transaction) end it 'increments the total and specific cache duration' do - expect(transaction).to receive(:increment). - with(:cache_duration, event.duration) + expect(transaction).to receive(:increment) + .with(:cache_duration, event.duration) - expect(transaction).to receive(:increment). - with(:cache_count, 1) + expect(transaction).to receive(:increment) + .with(:cache_count, 1) - expect(transaction).to receive(:increment). - with(:cache_delete_duration, event.duration) + expect(transaction).to receive(:increment) + .with(:cache_delete_duration, event.duration) - expect(transaction).to receive(:increment). - with(:cache_delete_count, 1) + expect(transaction).to receive(:increment) + .with(:cache_delete_count, 1) subscriber.increment(:cache_delete, event.duration) end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 0c5a6246d85..3779af81512 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -39,8 +39,8 @@ describe Gitlab::Metrics::Transaction do describe '#add_metric' do it 'adds a metric to the transaction' do - expect(Gitlab::Metrics::Metric).to receive(:new). - with('rails_foo', { number: 10 }, {}) + expect(Gitlab::Metrics::Metric).to receive(:new) + .with('rails_foo', { number: 10 }, {}) transaction.add_metric('foo', number: 10) end @@ -61,8 +61,8 @@ describe Gitlab::Metrics::Transaction do values = { duration: 0.0, time: 3, allocated_memory: a_kind_of(Numeric) } - expect(transaction).to receive(:add_metric). - with('transactions', values, {}) + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) transaction.track_self end @@ -78,8 +78,8 @@ describe Gitlab::Metrics::Transaction do allocated_memory: a_kind_of(Numeric) } - expect(transaction).to receive(:add_metric). - with('transactions', values, {}) + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) transaction.track_self end @@ -109,8 +109,8 @@ describe Gitlab::Metrics::Transaction do allocated_memory: a_kind_of(Numeric) } - expect(transaction).to receive(:add_metric). - with('transactions', values, {}) + expect(transaction).to receive(:add_metric) + .with('transactions', values, {}) transaction.track_self end @@ -120,8 +120,8 @@ describe Gitlab::Metrics::Transaction do it 'submits the metrics to Sidekiq' do transaction.track_self - expect(Gitlab::Metrics).to receive(:submit_metrics). - with([an_instance_of(Hash)]) + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([an_instance_of(Hash)]) transaction.submit end @@ -137,8 +137,8 @@ describe Gitlab::Metrics::Transaction do timestamp: a_kind_of(Integer) } - expect(Gitlab::Metrics).to receive(:submit_metrics). - with([hash]) + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([hash]) transaction.submit end @@ -154,8 +154,8 @@ describe Gitlab::Metrics::Transaction do timestamp: a_kind_of(Integer) } - expect(Gitlab::Metrics).to receive(:submit_metrics). - with([hash]) + expect(Gitlab::Metrics).to receive(:submit_metrics) + .with([hash]) transaction.submit end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 5a87b906609..599b8807d8d 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -15,6 +15,36 @@ describe Gitlab::Metrics do end end + describe '.prometheus_metrics_enabled_unmemoized' do + subject { described_class.send(:prometheus_metrics_enabled_unmemoized) } + + context 'prometheus metrics enabled in config' do + before do + allow(described_class).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true) + end + + context 'when metrics folder is present' do + before do + allow(described_class).to receive(:metrics_folder_present?).and_return(true) + end + + it 'metrics are enabled' do + expect(subject).to eq(true) + end + end + + context 'when metrics folder is missing' do + before do + allow(described_class).to receive(:metrics_folder_present?).and_return(false) + end + + it 'metrics are disabled' do + expect(subject).to eq(false) + end + end + end + end + describe '.prometheus_metrics_enabled?' do it 'returns a boolean' do expect(described_class.prometheus_metrics_enabled?).to be_in([true, false]) @@ -42,8 +72,8 @@ describe Gitlab::Metrics do describe '.prepare_metrics' do it 'returns a Hash with the keys as Symbols' do - metrics = described_class. - prepare_metrics([{ 'values' => {}, 'tags' => {} }]) + metrics = described_class + .prepare_metrics([{ 'values' => {}, 'tags' => {} }]) expect(metrics).to eq([{ values: {}, tags: {} }]) end @@ -88,19 +118,19 @@ describe Gitlab::Metrics do let(:transaction) { Gitlab::Metrics::Transaction.new } before do - allow(described_class).to receive(:current_transaction). - and_return(transaction) + allow(described_class).to receive(:current_transaction) + .and_return(transaction) end it 'adds a metric to the current transaction' do - expect(transaction).to receive(:increment). - with('foo_real_time', a_kind_of(Numeric)) + expect(transaction).to receive(:increment) + .with('foo_real_time', a_kind_of(Numeric)) - expect(transaction).to receive(:increment). - with('foo_cpu_time', a_kind_of(Numeric)) + expect(transaction).to receive(:increment) + .with('foo_cpu_time', a_kind_of(Numeric)) - expect(transaction).to receive(:increment). - with('foo_call_count', 1) + expect(transaction).to receive(:increment) + .with('foo_call_count', 1) described_class.measure(:foo) { 10 } end @@ -116,8 +146,8 @@ describe Gitlab::Metrics do describe '.tag_transaction' do context 'without a transaction' do it 'does nothing' do - expect_any_instance_of(Gitlab::Metrics::Transaction). - not_to receive(:add_tag) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .not_to receive(:add_tag) described_class.tag_transaction(:foo, 'bar') end @@ -127,11 +157,11 @@ describe Gitlab::Metrics do let(:transaction) { Gitlab::Metrics::Transaction.new } it 'adds the tag to the transaction' do - expect(described_class).to receive(:current_transaction). - and_return(transaction) + expect(described_class).to receive(:current_transaction) + .and_return(transaction) - expect(transaction).to receive(:add_tag). - with(:foo, 'bar') + expect(transaction).to receive(:add_tag) + .with(:foo, 'bar') described_class.tag_transaction(:foo, 'bar') end @@ -141,8 +171,8 @@ describe Gitlab::Metrics do describe '.action=' do context 'without a transaction' do it 'does nothing' do - expect_any_instance_of(Gitlab::Metrics::Transaction). - not_to receive(:action=) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .not_to receive(:action=) described_class.action = 'foo' end @@ -152,8 +182,8 @@ describe Gitlab::Metrics do it 'sets the action of a transaction' do trans = Gitlab::Metrics::Transaction.new - expect(described_class).to receive(:current_transaction). - and_return(trans) + expect(described_class).to receive(:current_transaction) + .and_return(trans) expect(trans).to receive(:action=).with('foo') @@ -171,8 +201,8 @@ describe Gitlab::Metrics do describe '.add_event' do context 'without a transaction' do it 'does nothing' do - expect_any_instance_of(Gitlab::Metrics::Transaction). - not_to receive(:add_event) + expect_any_instance_of(Gitlab::Metrics::Transaction) + .not_to receive(:add_event) described_class.add_event(:meow) end @@ -184,8 +214,8 @@ describe Gitlab::Metrics do expect(transaction).to receive(:add_event).with(:meow) - expect(described_class).to receive(:current_transaction). - and_return(transaction) + expect(described_class).to receive(:current_transaction) + .and_return(transaction) described_class.add_event(:meow) end diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb index 168090d5b5c..88107536c9e 100644 --- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -6,7 +6,9 @@ describe Gitlab::Middleware::RailsQueueDuration do let(:env) { {} } let(:transaction) { double(:transaction) } - before { expect(app).to receive(:call).with(env).and_return('yay') } + before do + expect(app).to receive(:call).with(env).and_return('yay') + end describe '#call' do it 'calls the app when metrics are disabled' do @@ -15,7 +17,9 @@ describe Gitlab::Middleware::RailsQueueDuration do end context 'when metrics are enabled' do - before { allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) } + before do + allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) + end it 'calls the app when metrics are enabled but no timing header is found' do expect(middleware.call(env)).to eq('yay') diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index 8aaeb5779d3..19ab17419fc 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -55,7 +55,9 @@ describe Gitlab::OAuth::AuthHash, lib: true do end context 'email not provided' do - before { info_hash.delete(:email) } + before do + info_hash.delete(:email) + end it 'generates a temp email' do expect( auth_hash.email).to start_with('temp-email-for-oauth') @@ -63,7 +65,9 @@ describe Gitlab::OAuth::AuthHash, lib: true do end context 'username not provided' do - before { info_hash.delete(:nickname) } + before do + info_hash.delete(:nickname) + end it 'takes the first part of the email as username' do expect(auth_hash.username).to eql 'onur.kucuk_ABC-123' @@ -71,7 +75,9 @@ describe Gitlab::OAuth::AuthHash, lib: true do end context 'name not provided' do - before { info_hash.delete(:name) } + before do + info_hash.delete(:name) + end it 'concats first and lastname as the name' do expect(auth_hash.name).to eql name_utf8 diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 8943d1aa488..ea29cb9caf1 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -112,7 +112,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'with new allow_single_sign_on enabled syntax' do - before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } + before do + stub_omniauth_config(allow_single_sign_on: ['twitter']) + end it "creates a user from Omniauth" do oauth_user.save @@ -125,7 +127,9 @@ describe Gitlab::OAuth::User, lib: true do end context "with old allow_single_sign_on enabled syntax" do - before { stub_omniauth_config(allow_single_sign_on: true) } + before do + stub_omniauth_config(allow_single_sign_on: true) + end it "creates a user from Omniauth" do oauth_user.save @@ -138,14 +142,20 @@ describe Gitlab::OAuth::User, lib: true do end context 'with new allow_single_sign_on disabled syntax' do - before { stub_omniauth_config(allow_single_sign_on: []) } + before do + stub_omniauth_config(allow_single_sign_on: []) + end + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end context 'with old allow_single_sign_on disabled (Default)' do - before { stub_omniauth_config(allow_single_sign_on: false) } + before do + stub_omniauth_config(allow_single_sign_on: false) + end + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end @@ -153,21 +163,30 @@ describe Gitlab::OAuth::User, lib: true do end context "with auto_link_ldap_user disabled (default)" do - before { stub_omniauth_config(auto_link_ldap_user: false) } + before do + stub_omniauth_config(auto_link_ldap_user: false) + end + include_examples "to verify compliance with allow_single_sign_on" end context "with auto_link_ldap_user enabled" do - before { stub_omniauth_config(auto_link_ldap_user: true) } + before do + stub_omniauth_config(auto_link_ldap_user: true) + end context "and no LDAP provider defined" do - before { stub_ldap_config(providers: []) } + before do + stub_ldap_config(providers: []) + end include_examples "to verify compliance with allow_single_sign_on" end context "and at least one LDAP provider is defined" do - before { stub_ldap_config(providers: %w(ldapmain)) } + before do + stub_ldap_config(providers: %w(ldapmain)) + end context "and a corresponding LDAP person" do before do @@ -238,7 +257,9 @@ describe Gitlab::OAuth::User, lib: true do end context "and no corresponding LDAP person" do - before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) } + before do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) + end include_examples "to verify compliance with allow_single_sign_on" end @@ -248,11 +269,16 @@ describe Gitlab::OAuth::User, lib: true do describe 'blocking' do let(:provider) { 'twitter' } - before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } + + before do + stub_omniauth_config(allow_single_sign_on: ['twitter']) + end context 'signup with omniauth only' do context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it do oauth_user.save @@ -262,7 +288,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it do oauth_user.save @@ -284,7 +312,9 @@ describe Gitlab::OAuth::User, lib: true do context "and no account for the LDAP user" do context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + end it do oauth_user.save @@ -294,7 +324,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + end it do oauth_user.save @@ -308,7 +340,9 @@ describe Gitlab::OAuth::User, lib: true do let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + end it do oauth_user.save @@ -318,7 +352,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + end it do oauth_user.save @@ -336,7 +372,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it do oauth_user.save @@ -346,7 +384,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it do oauth_user.save @@ -356,7 +396,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + end it do oauth_user.save @@ -366,7 +408,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + end it do oauth_user.save diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb index 67321f43710..9ce33685697 100644 --- a/spec/lib/gitlab/project_authorizations_spec.rb +++ b/spec/lib/gitlab/project_authorizations_spec.rb @@ -34,8 +34,8 @@ describe Gitlab::ProjectAuthorizations do end it 'includes the correct projects' do - expect(authorizations.pluck(:project_id)). - to include(owned_project.id, other_project.id, group_project.id) + expect(authorizations.pluck(:project_id)) + .to include(owned_project.id, other_project.id, group_project.id) end it 'includes the correct access levels' do diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb new file mode 100644 index 00000000000..61d48b05454 --- /dev/null +++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::AdditionalMetricsParser, lib: true do + include Prometheus::MetricBuilders + + let(:parser_error_class) { Gitlab::Prometheus::ParsingError } + + describe '#load_groups_from_yaml' do + subject { described_class.load_groups_from_yaml } + + describe 'parsing sample yaml' do + let(:sample_yaml) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: "title" + required_metrics: [ metric_a, metric_b ] + weight: 1 + queries: [{ query_range: 'query_range_a', label: label, unit: unit }] + - title: "title" + required_metrics: [metric_a] + weight: 1 + queries: [{ query_range: 'query_range_empty' }] + - group: group_b + priority: 1 + metrics: + - title: title + required_metrics: ['metric_a'] + weight: 1 + queries: [{query_range: query_range_a}] + EOF + end + + before do + allow(described_class).to receive(:load_yaml_file) { YAML.load(sample_yaml) } + end + + it 'parses to two metric groups with 2 and 1 metric respectively' do + expect(subject.count).to eq(2) + expect(subject[0].metrics.count).to eq(2) + expect(subject[1].metrics.count).to eq(1) + end + + it 'provide group data' do + expect(subject[0]).to have_attributes(name: 'group_a', priority: 1) + expect(subject[1]).to have_attributes(name: 'group_b', priority: 1) + end + + it 'provides metrics data' do + metrics = subject.flat_map(&:metrics) + + expect(metrics.count).to eq(3) + expect(metrics[0]).to have_attributes(title: 'title', required_metrics: %w(metric_a metric_b), weight: 1) + expect(metrics[1]).to have_attributes(title: 'title', required_metrics: %w(metric_a), weight: 1) + expect(metrics[2]).to have_attributes(title: 'title', required_metrics: %w{metric_a}, weight: 1) + end + + it 'provides query data' do + queries = subject.flat_map(&:metrics).flat_map(&:queries) + + expect(queries.count).to eq(3) + expect(queries[0]).to eq(query_range: 'query_range_a', label: 'label', unit: 'unit') + expect(queries[1]).to eq(query_range: 'query_range_empty') + expect(queries[2]).to eq(query_range: 'query_range_a') + end + end + + shared_examples 'required field' do |field_name| + context "when #{field_name} is nil" do + before do + allow(described_class).to receive(:load_yaml_file) { YAML.load(field_missing) } + end + + it 'throws parsing error' do + expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i) + end + end + + context "when #{field_name} are not specified" do + before do + allow(described_class).to receive(:load_yaml_file) { YAML.load(field_nil) } + end + + it 'throws parsing error' do + expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i) + end + end + end + + describe 'group required fields' do + it_behaves_like 'required field', 'metrics' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + EOF + end + end + + it_behaves_like 'required field', 'name' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: + priority: 1 + metrics: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - priority: 1 + metrics: [] + EOF + end + end + + it_behaves_like 'required field', 'priority' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: + metrics: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + metrics: [] + EOF + end + end + end + + describe 'metrics fields parsing' do + it_behaves_like 'required field', 'title' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: + required_metrics: [] + weight: 1 + queries: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - required_metrics: [] + weight: 1 + queries: [] + EOF + end + end + + it_behaves_like 'required field', 'required metrics' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: + weight: 1 + queries: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + weight: 1 + queries: [] + EOF + end + end + + it_behaves_like 'required field', 'weight' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + weight: + queries: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + queries: [] + EOF + end + end + + it_behaves_like 'required field', :queries do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + weight: 1 + queries: + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + weight: 1 + EOF + end + end + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb new file mode 100644 index 00000000000..4909aec5a4d --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery, lib: true do + include Prometheus::MetricBuilders + + let(:client) { double('prometheus_client') } + let(:environment) { create(:environment, slug: 'environment-slug') } + let(:deployment) { create(:deployment, environment: environment) } + + subject(:query_result) { described_class.new(client).query(deployment.id) } + + around do |example| + Timecop.freeze(Time.local(2008, 9, 1, 12, 0, 0)) { example.run } + end + + include_examples 'additional metrics query' do + it 'queries using specific time' do + expect(client).to receive(:query_range).with(anything, + start: (deployment.created_at - 30.minutes).to_f, + stop: (deployment.created_at + 30.minutes).to_f) + + expect(query_result).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb new file mode 100644 index 00000000000..8e6e3bb5946 --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery, lib: true do + include Prometheus::MetricBuilders + + let(:client) { double('prometheus_client') } + let(:environment) { create(:environment, slug: 'environment-slug') } + + subject(:query_result) { described_class.new(client).query(environment.id) } + + around do |example| + Timecop.freeze { example.run } + end + + include_examples 'additional metrics query' do + it 'queries using specific time' do + expect(client).to receive(:query_range).with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) + expect(query_result).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb b/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb new file mode 100644 index 00000000000..d2796ab72da --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::MatchedMetricsQuery, lib: true do + include Prometheus::MetricBuilders + + let(:metric_group_class) { Gitlab::Prometheus::MetricGroup } + let(:metric_class) { Gitlab::Prometheus::Metric } + + def series_info_with_environment(*more_metrics) + %w{metric_a metric_b}.concat(more_metrics).map { |metric_name| { '__name__' => metric_name, 'environment' => '' } } + end + + let(:metric_names) { %w{metric_a metric_b} } + let(:series_info_without_environment) do + [{ '__name__' => 'metric_a' }, + { '__name__' => 'metric_b' }] + end + let(:partialy_empty_series_info) { [{ '__name__' => 'metric_a', 'environment' => '' }] } + let(:empty_series_info) { [] } + + let(:client) { double('prometheus_client') } + + subject { described_class.new(client) } + + context 'with one group where two metrics is found' do + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + allow(client).to receive(:label_values).and_return(metric_names) + end + + context 'both metrics in the group pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_with_environment) + end + + it 'responds with both metrics as actve' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 2, metrics_missing_requirements: 0 }]) + end + end + + context 'none of the metrics pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_without_environment) + end + + it 'responds with both metrics missing requirements' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 }]) + end + end + + context 'no series information found about the metrics' do + before do + allow(client).to receive(:series).and_return(empty_series_info) + end + + it 'responds with both metrics missing requirements' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 }]) + end + end + + context 'one of the series info was not found' do + before do + allow(client).to receive(:series).and_return(partialy_empty_series_info) + end + it 'responds with one active and one missing metric' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 1 }]) + end + end + end + + context 'with one group where only one metric is found' do + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + allow(client).to receive(:label_values).and_return('metric_a') + end + + context 'both metrics in the group pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_with_environment) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 0 }]) + end + end + + context 'no metrics in group pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_without_environment) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 1 }]) + end + end + end + + context 'with two groups where metrics are found in each group' do + let(:second_metric_group) { simple_metric_group(name: 'nameb', metrics: simple_metrics(added_metric_name: 'metric_c')) } + + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group, second_metric_group]) + allow(client).to receive(:label_values).and_return('metric_c') + end + + context 'all metrics in both groups pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_with_environment('metric_c')) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([ + { group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 0 }, + { group: 'nameb', priority: 1, active_metrics: 2, metrics_missing_requirements: 0 } + ] + ) + end + end + + context 'no metrics in groups pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_without_environment) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([ + { group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 1 }, + { group: 'nameb', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 } + ] + ) + end + end + end +end diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 2d8bd2f6b97..46eaadae206 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -119,6 +119,36 @@ describe Gitlab::PrometheusClient, lib: true do end end + describe '#series' do + let(:query_url) { prometheus_series_url('series_name', 'other_service') } + + around do |example| + Timecop.freeze { example.run } + end + + it 'calls endpoint and returns list of series' do + req_stub = stub_prometheus_request(query_url, body: prometheus_series('series_name')) + expected = prometheus_series('series_name').deep_stringify_keys['data'] + + expect(subject.series('series_name', 'other_service')).to eq(expected) + + expect(req_stub).to have_been_requested + end + end + + describe '#label_values' do + let(:query_url) { prometheus_label_values_url('__name__') } + + it 'calls endpoint and returns label values' do + req_stub = stub_prometheus_request(query_url, body: prometheus_label_values) + expected = prometheus_label_values.deep_stringify_keys['data'] + + expect(subject.label_values('__name__')).to eq(expected) + + expect(req_stub).to have_been_requested + end + end + describe '#query_range' do let(:prometheus_query) { prometheus_memory_query('env-slug') } let(:query_url) { prometheus_query_range_url(prometheus_query) } diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb index 5b9173d3d3f..f44a562dc63 100644 --- a/spec/lib/gitlab/slash_commands/command_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::SlashCommands::CommandDefinition do +describe Gitlab::QuickActions::CommandDefinition do subject { described_class.new(:command) } describe "#all_names" do diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb index 33b49a5ddf9..a4bb3f911d7 100644 --- a/spec/lib/gitlab/slash_commands/dsl_spec.rb +++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::SlashCommands::Dsl do +describe Gitlab::QuickActions::Dsl do before :all do DummyClass = Struct.new(:project) do - include Gitlab::SlashCommands::Dsl # rubocop:disable RSpec/DescribedClass + include Gitlab::QuickActions::Dsl # rubocop:disable RSpec/DescribedClass desc 'A command with no args' command :no_args, :none do diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb index d7f77486b3e..9d32938e155 100644 --- a/spec/lib/gitlab/slash_commands/extractor_spec.rb +++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::SlashCommands::Extractor do +describe Gitlab::QuickActions::Extractor do let(:definitions) do Class.new do - include Gitlab::SlashCommands::Dsl + include Gitlab::QuickActions::Dsl command(:reopen, :open) { } command(:assign) { } diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index 8b77c925705..593aa5038ad 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -108,11 +108,18 @@ describe Gitlab::Redis do end describe '.with' do - before { clear_pool } - after { clear_pool } + before do + clear_pool + end + + after do + clear_pool + end context 'when running not on sidekiq workers' do - before { allow(Sidekiq).to receive(:server?).and_return(false) } + before do + allow(Sidekiq).to receive(:server?).and_return(false) + end it 'instantiates a connection pool with size 5' do expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 0bee892fe0c..979f4fefcb6 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -21,6 +21,18 @@ describe Gitlab::Regex, lib: true do end describe '.environment_slug_regex' do + subject { described_class.environment_name_regex } + + it { is_expected.to match('foo') } + it { is_expected.to match('foo-1') } + it { is_expected.to match('FOO') } + it { is_expected.to match('foo/1') } + it { is_expected.to match('foo.1') } + it { is_expected.not_to match('9&foo') } + it { is_expected.not_to match('foo-^') } + end + + describe '.environment_slug_regex' do subject { described_class.environment_slug_regex } it { is_expected.to match('foo') } diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index f9025397107..efea4f429bf 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -4,24 +4,44 @@ describe ::Gitlab::RepoPath do describe '.parse' do set(:project) { create(:project) } - it 'parses a full repository path' do - expect(described_class.parse(project.repository.path)).to eq([project, false]) - end + context 'a repository storage path' do + it 'parses a full repository path' do + expect(described_class.parse(project.repository.path)).to eq([project, false, nil]) + end - it 'parses a full wiki path' do - expect(described_class.parse(project.wiki.repository.path)).to eq([project, true]) + it 'parses a full wiki path' do + expect(described_class.parse(project.wiki.repository.path)).to eq([project, true, nil]) + end end - it 'parses a relative repository path' do - expect(described_class.parse(project.full_path + '.git')).to eq([project, false]) - end + context 'a relative path' do + it 'parses a relative repository path' do + expect(described_class.parse(project.full_path + '.git')).to eq([project, false, nil]) + end - it 'parses a relative wiki path' do - expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true]) - end + it 'parses a relative wiki path' do + expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true, nil]) + end + + it 'parses a relative path starting with /' do + expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false, nil]) + end + + context 'of a redirected project' do + let(:redirect) { project.route.create_redirect('foo/bar') } + + it 'parses a relative repository path' do + expect(described_class.parse(redirect.path + '.git')).to eq([project, false, 'foo/bar']) + end + + it 'parses a relative wiki path' do + expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, true, 'foo/bar.wiki']) + end - it 'parses a relative path starting with /' do - expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false]) + it 'parses a relative path starting with /' do + expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, false, 'foo/bar']) + end + end end end @@ -43,4 +63,33 @@ describe ::Gitlab::RepoPath do ) end end + + describe '.find_project' do + let(:project) { create(:empty_project) } + let(:redirect) { project.route.create_redirect('foo/bar/baz') } + + context 'when finding a project by its canonical path' do + context 'when the cases match' do + it 'returns the project and false' do + expect(described_class.find_project(project.full_path)).to eq([project, false]) + end + end + + context 'when the cases do not match' do + # This is slightly different than web behavior because on the web it is + # easy and safe to redirect someone to the correctly-cased URL. For git + # requests, we should accept wrongly-cased URLs because it is a pain to + # block people's git operations and force them to update remote URLs. + it 'returns the project and false' do + expect(described_class.find_project(project.full_path.upcase)).to eq([project, false]) + end + end + end + + context 'when finding a project via a redirect' do + it 'returns the project and true' do + expect(described_class.find_project(redirect.path)).to eq([project, true]) + end + end + end end diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb index 2370f56a613..21c00c6e5b8 100644 --- a/spec/lib/gitlab/route_map_spec.rb +++ b/spec/lib/gitlab/route_map_spec.rb @@ -4,43 +4,43 @@ describe Gitlab::RouteMap, lib: true do describe '#initialize' do context 'when the data is not YAML' do it 'raises an error' do - expect { described_class.new('"') }. - to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/) + expect { described_class.new('"') } + .to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/) end end context 'when the data is not a YAML array' do it 'raises an error' do - expect { described_class.new(YAML.dump('foo')) }. - to raise_error(Gitlab::RouteMap::FormatError, /an array/) + expect { described_class.new(YAML.dump('foo')) } + .to raise_error(Gitlab::RouteMap::FormatError, /an array/) end end context 'when an entry is not a hash' do it 'raises an error' do - expect { described_class.new(YAML.dump(['foo'])) }. - to raise_error(Gitlab::RouteMap::FormatError, /a hash/) + expect { described_class.new(YAML.dump(['foo'])) } + .to raise_error(Gitlab::RouteMap::FormatError, /a hash/) end end context 'when an entry does not have a source key' do it 'raises an error' do - expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) }. - to raise_error(Gitlab::RouteMap::FormatError, /source key/) + expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) } + .to raise_error(Gitlab::RouteMap::FormatError, /source key/) end end context 'when an entry does not have a public key' do it 'raises an error' do - expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) }. - to raise_error(Gitlab::RouteMap::FormatError, /public key/) + expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) } + .to raise_error(Gitlab::RouteMap::FormatError, /public key/) end end context 'when an entry source is not a valid regex' do it 'raises an error' do - expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) }. - to raise_error(Gitlab::RouteMap::FormatError, /regular expression/) + expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) } + .to raise_error(Gitlab::RouteMap::FormatError, /regular expression/) end end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index b106d156b75..a4d2367b72a 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -31,11 +31,17 @@ describe Gitlab::Saml::User, lib: true do allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) end - before { stub_basic_saml_config } + before do + stub_basic_saml_config + end describe 'account exists on server' do - before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + before do + stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) + end + let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } + context 'and should bind with SAML' do it 'adds the SAML identity to the existing user' do saml_user.save @@ -57,7 +63,10 @@ describe Gitlab::Saml::User, lib: true do end end - before { stub_saml_group_config(%w(Interns)) } + before do + stub_saml_group_config(%w(Interns)) + end + context 'are defined but the user does not belong there' do it 'does not mark the user as external' do saml_user.save @@ -80,7 +89,9 @@ describe Gitlab::Saml::User, lib: true do describe 'no account exists on server' do shared_examples 'to verify compliance with allow_single_sign_on' do context 'with allow_single_sign_on enabled' do - before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + before do + stub_omniauth_config(allow_single_sign_on: ['saml']) + end it 'creates a user from SAML' do saml_user.save @@ -93,14 +104,20 @@ describe Gitlab::Saml::User, lib: true do end context 'with allow_single_sign_on default (["saml"])' do - before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + before do + stub_omniauth_config(allow_single_sign_on: ['saml']) + end + it 'does not throw an error' do expect{ saml_user.save }.not_to raise_error end end context 'with allow_single_sign_on disabled' do - before { stub_omniauth_config(allow_single_sign_on: false) } + before do + stub_omniauth_config(allow_single_sign_on: false) + end + it 'throws an error' do expect{ saml_user.save }.to raise_error StandardError end @@ -128,15 +145,22 @@ describe Gitlab::Saml::User, lib: true do end context 'with auto_link_ldap_user disabled (default)' do - before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) } + before do + stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) + end + include_examples 'to verify compliance with allow_single_sign_on' end context 'with auto_link_ldap_user enabled' do - before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) } + before do + stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) + end context 'and at least one LDAP provider is defined' do - before { stub_ldap_config(providers: %w(ldapmain)) } + before do + stub_ldap_config(providers: %w(ldapmain)) + end context 'and a corresponding LDAP person' do before do @@ -239,11 +263,15 @@ describe Gitlab::Saml::User, lib: true do end describe 'blocking' do - before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + before do + stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) + end context 'signup with SAML only' do context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it 'does not block the user' do saml_user.save @@ -253,7 +281,9 @@ describe Gitlab::Saml::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it 'blocks user' do saml_user.save @@ -270,7 +300,9 @@ describe Gitlab::Saml::User, lib: true do end context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it do saml_user.save @@ -280,7 +312,9 @@ describe Gitlab::Saml::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it do saml_user.save diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb index 519eb1b274f..1bc6536439e 100644 --- a/spec/lib/gitlab/serializer/pagination_spec.rb +++ b/spec/lib/gitlab/serializer/pagination_spec.rb @@ -22,7 +22,9 @@ describe Gitlab::Serializer::Pagination do let(:params) { { page: 1, per_page: 2 } } context 'when a multiple resources are present in relation' do - before { create_list(:user, 3) } + before do + create_list(:user, 3) + end it 'correctly paginates the resource' do expect(subject.count).to be 2 diff --git a/spec/lib/gitlab/sherlock/file_sample_spec.rb b/spec/lib/gitlab/sherlock/file_sample_spec.rb index cadf8bbce78..4989d14def3 100644 --- a/spec/lib/gitlab/sherlock/file_sample_spec.rb +++ b/spec/lib/gitlab/sherlock/file_sample_spec.rb @@ -35,8 +35,8 @@ describe Gitlab::Sherlock::FileSample, lib: true do describe '#relative_path' do it 'returns the relative path' do - expect(sample.relative_path). - to eq('spec/lib/gitlab/sherlock/file_sample_spec.rb') + expect(sample.relative_path) + .to eq('spec/lib/gitlab/sherlock/file_sample_spec.rb') end end diff --git a/spec/lib/gitlab/sherlock/line_profiler_spec.rb b/spec/lib/gitlab/sherlock/line_profiler_spec.rb index d57627bba2b..39c6b2a4844 100644 --- a/spec/lib/gitlab/sherlock/line_profiler_spec.rb +++ b/spec/lib/gitlab/sherlock/line_profiler_spec.rb @@ -20,9 +20,9 @@ describe Gitlab::Sherlock::LineProfiler, lib: true do describe '#profile_mri' do it 'returns an Array containing the return value and profiling samples' do - allow(profiler).to receive(:lineprof). - and_yield. - and_return({ __FILE__ => [[0, 0, 0, 0]] }) + allow(profiler).to receive(:lineprof) + .and_yield + .and_return({ __FILE__ => [[0, 0, 0, 0]] }) retval, samples = profiler.profile_mri { 42 } diff --git a/spec/lib/gitlab/sherlock/middleware_spec.rb b/spec/lib/gitlab/sherlock/middleware_spec.rb index 2bbeb25ce98..b98ab0b14a2 100644 --- a/spec/lib/gitlab/sherlock/middleware_spec.rb +++ b/spec/lib/gitlab/sherlock/middleware_spec.rb @@ -72,8 +72,8 @@ describe Gitlab::Sherlock::Middleware, lib: true do 'REQUEST_URI' => '/cats' } - expect(middleware.transaction_from_env(env)). - to be_an_instance_of(Gitlab::Sherlock::Transaction) + expect(middleware.transaction_from_env(env)) + .to be_an_instance_of(Gitlab::Sherlock::Transaction) end end end diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb index 0a620428138..d97b5eef573 100644 --- a/spec/lib/gitlab/sherlock/query_spec.rb +++ b/spec/lib/gitlab/sherlock/query_spec.rb @@ -13,8 +13,8 @@ describe Gitlab::Sherlock::Query, lib: true do sql = 'SELECT COUNT(*) FROM users WHERE id = $1' bindings = [[double(:column), 10]] - query = described_class. - new_with_bindings(sql, bindings, started_at, finished_at) + query = described_class + .new_with_bindings(sql, bindings, started_at, finished_at) expect(query.query).to eq('SELECT COUNT(*) FROM users WHERE id = 10;') end diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb index 9fe18f253f0..6ae1aa20ea7 100644 --- a/spec/lib/gitlab/sherlock/transaction_spec.rb +++ b/spec/lib/gitlab/sherlock/transaction_spec.rb @@ -109,8 +109,8 @@ describe Gitlab::Sherlock::Transaction, lib: true do query1 = Gitlab::Sherlock::Query.new('SELECT 1', start_time, start_time) - query2 = Gitlab::Sherlock::Query. - new('SELECT 2', start_time, start_time + 5) + query2 = Gitlab::Sherlock::Query + .new('SELECT 2', start_time, start_time + 5) transaction.queries << query1 transaction.queries << query2 @@ -162,11 +162,11 @@ describe Gitlab::Sherlock::Transaction, lib: true do describe '#profile_lines' do describe 'when line profiling is enabled' do it 'yields the block using the line profiler' do - allow(Gitlab::Sherlock).to receive(:enable_line_profiler?). - and_return(true) + allow(Gitlab::Sherlock).to receive(:enable_line_profiler?) + .and_return(true) - allow_any_instance_of(Gitlab::Sherlock::LineProfiler). - to receive(:profile).and_return('cats are amazing', []) + allow_any_instance_of(Gitlab::Sherlock::LineProfiler) + .to receive(:profile).and_return('cats are amazing', []) retval = transaction.profile_lines { 'cats are amazing' } @@ -176,8 +176,8 @@ describe Gitlab::Sherlock::Transaction, lib: true do describe 'when line profiling is disabled' do it 'yields the block' do - allow(Gitlab::Sherlock).to receive(:enable_line_profiler?). - and_return(false) + allow(Gitlab::Sherlock).to receive(:enable_line_profiler?) + .and_return(false) retval = transaction.profile_lines { 'cats are amazing' } @@ -196,8 +196,8 @@ describe Gitlab::Sherlock::Transaction, lib: true do end it 'tracks executed queries' do - expect(transaction).to receive(:track_query). - with('SELECT 1', [], time, time) + expect(transaction).to receive(:track_query) + .with('SELECT 1', [], time, time) subscription.publish('test', time, time, nil, query_data) end @@ -205,8 +205,8 @@ describe Gitlab::Sherlock::Transaction, lib: true do it 'only tracks queries triggered from the transaction thread' do expect(transaction).not_to receive(:track_query) - Thread.new { subscription.publish('test', time, time, nil, query_data) }. - join + Thread.new { subscription.publish('test', time, time, nil, query_data) } + .join end end @@ -228,8 +228,8 @@ describe Gitlab::Sherlock::Transaction, lib: true do it 'only tracks views rendered from the transaction thread' do expect(transaction).not_to receive(:track_view) - Thread.new { subscription.publish('test', time, time, nil, view_data) }. - join + Thread.new { subscription.publish('test', time, time, nil, view_data) } + .join end end end diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb index 6307f8c16a3..37d9e1d3e6b 100644 --- a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb @@ -5,8 +5,8 @@ describe Gitlab::SidekiqStatus::ClientMiddleware do it 'tracks the job in Redis' do expect(Gitlab::SidekiqStatus).to receive(:set).with('123', Gitlab::SidekiqStatus::DEFAULT_EXPIRATION) - described_class.new. - call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil } + described_class.new + .call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil } end end end diff --git a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb index 80728197b8c..04e09d3dec8 100644 --- a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb @@ -5,8 +5,8 @@ describe Gitlab::SidekiqStatus::ServerMiddleware do it 'stops tracking of a job upon completion' do expect(Gitlab::SidekiqStatus).to receive(:unset).with('123') - ret = described_class.new. - call(double(:worker), { 'jid' => '123' }, double(:queue)) { 10 } + ret = described_class.new + .call(double(:worker), { 'jid' => '123' }, double(:queue)) { 10 } expect(ret).to eq(10) end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb index 13e6953147b..28d7f9858c3 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::Command, service: true do +describe Gitlab::SlashCommands::Command, service: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } @@ -93,19 +93,19 @@ describe Gitlab::ChatCommands::Command, service: true do context 'IssueShow is triggered' do let(:params) { { text: 'issue show 123' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) } + it { is_expected.to eq(Gitlab::SlashCommands::IssueShow) } end context 'IssueCreate is triggered' do let(:params) { { text: 'issue create my title' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) } + it { is_expected.to eq(Gitlab::SlashCommands::IssueNew) } end context 'IssueSearch is triggered' do let(:params) { { text: 'issue search my query' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) } + it { is_expected.to eq(Gitlab::SlashCommands::IssueSearch) } end end end diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb index 46dbdeae37c..d919f7260db 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::Deploy, service: true do +describe Gitlab::SlashCommands::Deploy, service: true do describe '#execute' do let(:project) { create(:empty_project) } let(:user) { create(:user) } diff --git a/spec/lib/gitlab/chat_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb index 84c22328064..4de50d4a8bb 100644 --- a/spec/lib/gitlab/chat_commands/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::IssueNew, service: true do +describe Gitlab::SlashCommands::IssueNew, service: true do describe '#execute' do let(:project) { create(:empty_project) } let(:user) { create(:user) } diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb index 551ccb79a58..06fff0afc50 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::IssueSearch, service: true do +describe Gitlab::SlashCommands::IssueSearch, service: true do describe '#execute' do let!(:issue) { create(:issue, project: project, title: 'find me') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/issue_show_spec.rb index 1f20d0a44ce..1899f664ccd 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::IssueShow, service: true do +describe Gitlab::SlashCommands::IssueShow, service: true do describe '#execute' do let(:issue) { create(:issue, project: project) } let(:project) { create(:empty_project) } diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb index ae41d75ab0c..ef3d217f7be 100644 --- a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::Presenters::Access do +describe Gitlab::SlashCommands::Presenters::Access do describe '#access_denied' do subject { described_class.new.access_denied } diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb index dc2dd300072..dee3c77db27 100644 --- a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::Presenters::Deploy do +describe Gitlab::SlashCommands::Presenters::Deploy do let(:build) { create(:ci_build) } describe '#present' do diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb index 17fcdbc2452..7f81ebb47db 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::Presenters::IssueNew do +describe Gitlab::SlashCommands::Presenters::IssueNew do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:attachment) { subject[:attachments].first } diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb index ec6d3e34a96..7e57a0addcb 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb @@ -1,10 +1,12 @@ require 'spec_helper' -describe Gitlab::ChatCommands::Presenters::IssueSearch do +describe Gitlab::SlashCommands::Presenters::IssueSearch do let(:project) { create(:empty_project) } let(:message) { subject[:text] } - before { create_list(:issue, 2, project: project) } + before do + create_list(:issue, 2, project: project) + end subject { described_class.new(project.issues).present } diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb index 3916fc704a4..2a6ed860737 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::Presenters::IssueShow do +describe Gitlab::SlashCommands::Presenters::IssueShow do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:attachment) { subject[:attachments].first } diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 329d1d74970..bf45c8d16d6 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -52,7 +52,10 @@ describe Gitlab::Template::IssueTemplate do context 'when repo is bare or empty' do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "returns empty array" do templates = subject.by_category('', empty_project) @@ -77,7 +80,9 @@ describe Gitlab::Template::IssueTemplate do context "when repo is empty" do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "raises file not found" do issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project) diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index 2b0056d9bab..8479f92c8df 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -52,7 +52,10 @@ describe Gitlab::Template::MergeRequestTemplate do context 'when repo is bare or empty' do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "returns empty array" do templates = subject.by_category('', empty_project) @@ -77,7 +80,9 @@ describe Gitlab::Template::MergeRequestTemplate do context "when repo is empty" do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "raises file not found" do issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project) diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index e8a37e8d77b..e9a6e273516 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -112,8 +112,8 @@ describe Gitlab::UrlBuilder, lib: true do it 'returns a proper URL' do project = build_stubbed(:empty_project) - expect { described_class.build(project) }. - to raise_error(NotImplementedError, 'No URL builder defined for Project') + expect { described_class.build(project) } + .to raise_error(NotImplementedError, 'No URL builder defined for Project') end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index b47e1b56fa9..3c7c7562b46 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -37,6 +37,7 @@ describe Gitlab::UsageData do deploy_keys deployments environments + in_review_folder groups issues keys diff --git a/spec/lib/gitlab/view/presenter/delegated_spec.rb b/spec/lib/gitlab/view/presenter/delegated_spec.rb index e9d4af54389..940a2ce6ebd 100644 --- a/spec/lib/gitlab/view/presenter/delegated_spec.rb +++ b/spec/lib/gitlab/view/presenter/delegated_spec.rb @@ -18,8 +18,8 @@ describe Gitlab::View::Presenter::Delegated do end it 'raise an error if the presentee already respond to method' do - expect { presenter_class.new(project, user: 'Jane Doe') }. - to raise_error Gitlab::View::Presenter::CannotOverrideMethodError + expect { presenter_class.new(project, user: 'Jane Doe') } + .to raise_error Gitlab::View::Presenter::CannotOverrideMethodError end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 3255c6f1ef7..db9d2807be6 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -18,4 +18,35 @@ describe Gitlab::VisibilityLevel, lib: true do expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE) end end + + describe '.levels_for_user' do + it 'returns all levels for an admin' do + user = build(:user, :admin) + + expect(described_class.levels_for_user(user)) + .to eq([Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns INTERNAL and PUBLIC for internal users' do + user = build(:user) + + expect(described_class.levels_for_user(user)) + .to eq([Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns PUBLIC for external users' do + user = build(:user, :external) + + expect(described_class.levels_for_user(user)) + .to eq([Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns PUBLIC when no user is given' do + expect(described_class.levels_for_user) + .to eq([Gitlab::VisibilityLevel::PUBLIC]) + end + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index b1999409170..493ff3bb5fb 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -202,7 +202,11 @@ describe Gitlab::Workhorse, lib: true do context 'when Gitaly is enabled' do let(:gitaly_params) do { - GitalyAddress: Gitlab::GitalyClient.address('default') + GitalyAddress: Gitlab::GitalyClient.address('default'), + GitalyServer: { + address: Gitlab::GitalyClient.address('default'), + token: Gitlab::GitalyClient.token('default') + } } end @@ -212,7 +216,6 @@ describe Gitlab::Workhorse, lib: true do it 'includes a Repository param' do repo_param = { Repository: { - path: repo_path, storage_name: 'default', relative_path: project.full_path + '.git' } } diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb index 18726754517..e7022bd06f8 100644 --- a/spec/lib/json_web_token/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -15,11 +15,15 @@ describe JSONWebToken::RSAToken do let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } - before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) } + before do + allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) + end context 'token' do context 'for valid key to be validated' do - before { rsa_token['key'] = 'value' } + before do + rsa_token['key'] = 'value' + end subject { JWT.decode(rsa_encoded, rsa_key) } diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb index 3d955e4d774..d7e7560d962 100644 --- a/spec/lib/json_web_token/token_spec.rb +++ b/spec/lib/json_web_token/token_spec.rb @@ -3,7 +3,10 @@ describe JSONWebToken::Token do context 'custom parameters' do let(:value) { 'value' } - before { token[:key] = value } + + before do + token[:key] = value + end it { expect(token[:key]).to eq(value) } it { expect(token.payload).to include(key: value) } diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index 4b5938edeb9..369e7b181b9 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -6,8 +6,8 @@ describe Mattermost::Command do before do Mattermost::Session.base_uri('http://mattermost.example.com') - allow_any_instance_of(Mattermost::Client).to receive(:with_session). - and_yield(Mattermost::Session.new(nil)) + allow_any_instance_of(Mattermost::Client).to receive(:with_session) + .and_yield(Mattermost::Session.new(nil)) end describe '#create' do @@ -20,12 +20,12 @@ describe Mattermost::Command do context 'for valid trigger word' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - with(body: { + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + .with(body: { team_id: 'abc', trigger: 'gitlab' - }.to_json). - to_return( + }.to_json) + .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, body: { token: 'token' }.to_json @@ -39,8 +39,8 @@ describe Mattermost::Command do context 'for error message' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - to_return( + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + .to_return( status: 500, headers: { 'Content-Type' => 'application/json' }, body: { diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 74d12e37181..be3908e8f6a 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -21,8 +21,8 @@ describe Mattermost::Session, type: :request do describe '#with session' do let(:location) { 'http://location.tld' } let!(:stub) do - WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). - to_return(headers: { 'location' => location }, status: 307) + WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login") + .to_return(headers: { 'location' => location }, status: 307) end context 'without oauth uri' do @@ -60,9 +60,9 @@ describe Mattermost::Session, type: :request do end before do - WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). - with(query: hash_including({ 'state' => state })). - to_return do |request| + WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete") + .with(query: hash_including({ 'state' => state })) + .to_return do |request| post "/oauth/token", client_id: doorkeeper.uid, client_secret: doorkeeper.secret, @@ -75,8 +75,8 @@ describe Mattermost::Session, type: :request do end end - WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). - to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) + WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout") + .to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) end it 'can setup a session' do diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index ac493fdb20f..e638ad7a2c9 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -4,8 +4,8 @@ describe Mattermost::Team do before do Mattermost::Session.base_uri('http://mattermost.example.com') - allow_any_instance_of(Mattermost::Client).to receive(:with_session). - and_yield(Mattermost::Session.new(nil)) + allow_any_instance_of(Mattermost::Client).to receive(:with_session) + .and_yield(Mattermost::Session.new(nil)) end describe '#all' do @@ -30,8 +30,8 @@ describe Mattermost::Team do end before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, body: response.to_json @@ -45,8 +45,8 @@ describe Mattermost::Team do context 'for error message' do before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + .to_return( status: 500, headers: { 'Content-Type' => 'application/json' }, body: { diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb index a5c6170cd7d..795f11ee1f8 100644 --- a/spec/lib/system_check/simple_executor_spec.rb +++ b/spec/lib/system_check/simple_executor_spec.rb @@ -75,6 +75,24 @@ describe SystemCheck::SimpleExecutor, lib: true do end end + class BugousCheck < SystemCheck::BaseCheck + CustomError = Class.new(StandardError) + set_name 'my bugous check' + + def check? + raise CustomError, 'omg' + end + end + + before do + @rainbow = Rainbow.enabled + Rainbow.enabled = false + end + + after do + Rainbow.enabled = @rainbow + end + describe '#component' do it 'returns stored component name' do expect(subject.component).to eq('Test') @@ -219,5 +237,11 @@ describe SystemCheck::SimpleExecutor, lib: true do end end end + + context 'when there is an exception' do + it 'rescues the exception' do + expect{ subject.run_check(BugousCheck) }.not_to raise_exception + end + end end end diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb index eb433c38873..bda892083b3 100644 --- a/spec/mailers/abuse_report_mailer_spec.rb +++ b/spec/mailers/abuse_report_mailer_spec.rb @@ -30,8 +30,8 @@ describe AbuseReportMailer do it 'returns early' do stub_application_setting(admin_notification_email: nil) - expect { described_class.notify(spy).deliver_now }. - not_to change { ActionMailer::Base.deliveries.count } + expect { described_class.notify(spy).deliver_now } + .not_to change { ActionMailer::Base.deliveries.count } end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ec6f6c42eac..980b24370d0 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -130,8 +130,13 @@ describe Notify do end context 'with a preferred language' do - before { Gitlab::I18n.locale = :es } - after { Gitlab::I18n.use_default_locale } + before do + Gitlab::I18n.locale = :es + end + + after do + Gitlab::I18n.use_default_locale + end it 'always generates the email using the default language' do is_expected.to have_body_text('foo, bar, and baz') @@ -581,7 +586,9 @@ describe Notify do let(:project) { create(:project, :repository) } let(:commit) { project.commit } - before(:each) { allow(note).to receive(:noteable).and_return(commit) } + before do + allow(note).to receive(:noteable).and_return(commit) + end subject { described_class.note_commit_email(recipient.id, note.id) } @@ -603,7 +610,10 @@ describe Notify do describe 'on a merge request' do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } + + before do + allow(note).to receive(:noteable).and_return(merge_request) + end subject { described_class.note_merge_request_email(recipient.id, note.id) } @@ -625,7 +635,10 @@ describe Notify do describe 'on an issue' do let(:issue) { create(:issue, project: project) } let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(issue) } + + before do + allow(note).to receive(:noteable).and_return(issue) + end subject { described_class.note_issue_email(recipient.id, note.id) } @@ -687,7 +700,9 @@ describe Notify do let(:commit) { project.commit } let(:note) { create(:discussion_note_on_commit, commit_id: commit.id, project: project, author: note_author) } - before(:each) { allow(note).to receive(:noteable).and_return(commit) } + before do + allow(note).to receive(:noteable).and_return(commit) + end subject { described_class.note_commit_email(recipient.id, note.id) } @@ -711,7 +726,10 @@ describe Notify do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, author: note_author) } let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } + + before do + allow(note).to receive(:noteable).and_return(merge_request) + end subject { described_class.note_merge_request_email(recipient.id, note.id) } @@ -735,7 +753,10 @@ describe Notify do let(:issue) { create(:issue, project: project) } let(:note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: note_author) } let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(issue) } + + before do + allow(note).to receive(:noteable).and_return(issue) + end subject { described_class.note_issue_email(recipient.id, note.id) } diff --git a/spec/migrations/README.md b/spec/migrations/README.md new file mode 100644 index 00000000000..05d4f35db72 --- /dev/null +++ b/spec/migrations/README.md @@ -0,0 +1,87 @@ +# Testing migrations + +In order to reliably test a migration, we need to test it against a database +schema that this migration has been written for. In order to achieve that we +have some _migration helpers_ and RSpec test tag, called `:migration`. + +If you want to write a test for a migration consider adding `:migration` tag to +the test signature, like `describe SomeMigrationClass, :migration`. + +## How does it work? + +Adding a `:migration` tag to a test signature injects a few before / after +hooks to the test. + +The most important change is that adding a `:migration` tag adds a `before` +hook that will revert all migrations to the point that a migration under test +is not yet migrated. + +In other words, our custom RSpec hooks will find a previous migration, and +migrate the database **down** to the previous migration version. + +With this approach you can test a migration against a database schema that this +migration has been written for. + +Use `migrate!` helper to run the migration that is under test. + +The `after` hook will migrate the database **up** and reinstitutes the latest +schema version, so that the process does not affect subsequent specs and +ensures proper isolation. + +## Available helpers + +Use `table` helper to create a temporary `ActiveRecord::Base` derived model +for a table. + +Use `migrate!` helper to run the migration that is under test. It will not only +run migration, but will also bump the schema version in the `schema_migrations` +table. It is necessary because in the `after` hook we trigger the rest of +the migrations, and we need to know where to start. + +See `spec/support/migrations_helpers.rb` for all the available helpers. + +## An example + +```ruby +require 'spec_helper' + +# Load a migration class. + +require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb') + +describe MigratePipelineStages, :migration do + + # Create test data - pipeline and CI/CD jobs. + + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + before do + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + end + + # Test the migration. + + it 'correctly migrates pipeline stages' do + expect(stages.count).to be_zero + + migrate! + + expect(stages.count).to eq 2 + expect(stages.all.pluck(:name)).to match_array %w[test build] + end +end +``` + +## Best practices + +1. Use only one test example per migration unless there is a good reason to +use more. +1. Note that this type of tests do not run within the transaction, we use +a truncation database cleanup strategy. Do not depend on transaction being +present. diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb index bd5f85b901d..65bea662b02 100644 --- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb +++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb') -describe AddHeadPipelineForEachMergeRequest do +describe AddHeadPipelineForEachMergeRequest, :truncate do let(:migration) { described_class.new } let!(:project) { create(:empty_project) } diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb new file mode 100644 index 00000000000..1396d12e5a9 --- /dev/null +++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns') + +describe ConvertCustomNotificationSettingsToColumns, :migration do + let(:settings_params) do + [ + { level: 0, events: [:new_note] }, # disabled, single event + { level: 3, events: [:new_issue, :reopen_issue, :close_issue, :reassign_issue] }, # global, multiple events + { level: 5, events: described_class::EMAIL_EVENTS }, # custom, all events + { level: 5, events: [] } # custom, no events + ] + end + + let(:notification_settings_before) do + settings_params.map do |params| + events = {} + + params[:events].each do |event| + events[event] = true + end + + user = create(:user) + create_params = { user_id: user.id, level: params[:level], events: events } + notification_setting = described_class::NotificationSetting.create(create_params) + + [notification_setting, params] + end + end + + let(:notification_settings_after) do + settings_params.map do |params| + events = {} + + params[:events].each do |event| + events[event] = true + end + + user = create(:user) + create_params = events.merge(user_id: user.id, level: params[:level]) + notification_setting = described_class::NotificationSetting.create(create_params) + + [notification_setting, params] + end + end + + describe '#up' do + it 'migrates all settings where a custom event is enabled, even if they are not currently using the custom level' do + notification_settings_before + + described_class.new.up + + notification_settings_before.each do |(notification_setting, params)| + notification_setting.reload + + expect(notification_setting.read_attribute_before_type_cast(:events)).to be_nil + expect(notification_setting.level).to eq(params[:level]) + + described_class::EMAIL_EVENTS.each do |event| + # We don't set the others to false, just let them default to nil + expected = params[:events].include?(event) || nil + + expect(notification_setting.read_attribute(event)).to eq(expected) + end + end + end + end + + describe '#down' do + it 'creates a custom events hash for all settings where at least one event is enabled' do + notification_settings_after + + described_class.new.down + + notification_settings_after.each do |(notification_setting, params)| + notification_setting.reload + + expect(notification_setting.level).to eq(params[:level]) + + if params[:events].empty? + # We don't migrate empty settings + expect(notification_setting.events).to eq({}) + else + described_class::EMAIL_EVENTS.each do |event| + expected = params[:events].include?(event) + + expect(notification_setting.events[event]).to eq(expected) + expect(notification_setting.read_attribute(event)).to be_nil + end + end + end + end + + it 'reverts the database to the state it was in before' do + notification_settings_before + + described_class.new.up + described_class.new.down + + notification_settings_before.each do |(notification_setting, params)| + notification_setting.reload + + expect(notification_setting.level).to eq(params[:level]) + + if params[:events].empty? + # We don't migrate empty settings + expect(notification_setting.events).to eq({}) + else + described_class::EMAIL_EVENTS.each do |event| + expected = params[:events].include?(event) + + expect(notification_setting.events[event]).to eq(expected) + expect(notification_setting.read_attribute(event)).to be_nil + end + end + end + end + end +end diff --git a/spec/migrations/migrate_build_stage_reference_spec.rb b/spec/migrations/migrate_build_stage_reference_again_spec.rb index 80b321860c2..6be480ce58e 100644 --- a/spec/migrations/migrate_build_stage_reference_spec.rb +++ b/spec/migrations/migrate_build_stage_reference_again_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20170526185921_migrate_build_stage_reference.rb') +require Rails.root.join('db', 'post_migrate', '20170526190000_migrate_build_stage_reference_again.rb') -describe MigrateBuildStageReference, :migration do +describe MigrateBuildStageReferenceAgain, :migration do ## # Create test data - pipeline and CI/CD jobs. # diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index 3db57595fa6..4223d2337a8 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -11,33 +11,33 @@ describe MigrateProcessCommitWorkerJobs do describe 'Project' do describe 'find_including_path' do it 'returns Project instances' do - expect(described_class::Project.find_including_path(project.id)). - to be_an_instance_of(described_class::Project) + expect(described_class::Project.find_including_path(project.id)) + .to be_an_instance_of(described_class::Project) end it 'selects the full path for every Project' do - migration_project = described_class::Project. - find_including_path(project.id) + migration_project = described_class::Project + .find_including_path(project.id) - expect(migration_project[:path_with_namespace]). - to eq(project.path_with_namespace) + expect(migration_project[:path_with_namespace]) + .to eq(project.path_with_namespace) end end describe '#repository_storage_path' do it 'returns the storage path for the repository' do - migration_project = described_class::Project. - find_including_path(project.id) + migration_project = described_class::Project + .find_including_path(project.id) - expect(File.directory?(migration_project.repository_storage_path)). - to eq(true) + expect(File.directory?(migration_project.repository_storage_path)) + .to eq(true) end end describe '#repository_path' do it 'returns the path to the repository' do - migration_project = described_class::Project. - find_including_path(project.id) + migration_project = described_class::Project + .find_including_path(project.id) expect(File.directory?(migration_project.repository_path)).to eq(true) end @@ -45,11 +45,11 @@ describe MigrateProcessCommitWorkerJobs do describe '#repository' do it 'returns a Rugged::Repository' do - migration_project = described_class::Project. - find_including_path(project.id) + migration_project = described_class::Project + .find_including_path(project.id) - expect(migration_project.repository). - to be_an_instance_of(Rugged::Repository) + expect(migration_project.repository) + .to be_an_instance_of(Rugged::Repository) end end end @@ -73,9 +73,9 @@ describe MigrateProcessCommitWorkerJobs do end it 'skips jobs using a project that no longer exists' do - allow(described_class::Project).to receive(:find_including_path). - with(project.id). - and_return(nil) + allow(described_class::Project).to receive(:find_including_path) + .with(project.id) + .and_return(nil) migration.up @@ -83,9 +83,9 @@ describe MigrateProcessCommitWorkerJobs do end it 'skips jobs using commits that no longer exist' do - allow_any_instance_of(Rugged::Repository).to receive(:lookup). - with(commit.oid). - and_raise(Rugged::OdbError) + allow_any_instance_of(Rugged::Repository).to receive(:lookup) + .with(commit.oid) + .and_raise(Rugged::OdbError) migration.up @@ -99,12 +99,12 @@ describe MigrateProcessCommitWorkerJobs do end it 'encodes data to UTF-8' do - allow_any_instance_of(Rugged::Repository).to receive(:lookup). - with(commit.oid). - and_return(commit) + allow_any_instance_of(Rugged::Repository).to receive(:lookup) + .with(commit.oid) + .and_return(commit) - allow(commit).to receive(:message). - and_return('김치'.force_encoding('BINARY')) + allow(commit).to receive(:message) + .and_return('김치'.force_encoding('BINARY')) migration.up diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb index 1db9bc002ae..e3b42b5eac8 100644 --- a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb +++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activities_to_users_last_activity_on.rb') -describe MigrateUserActivitiesToUsersLastActivityOn, :redis do +describe MigrateUserActivitiesToUsersLastActivityOn, :redis, :truncate do let(:migration) { described_class.new } let!(:user_active_1) { create(:user) } let!(:user_active_2) { create(:user) } diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb index 70f8e0d6082..afaa5d836a7 100644 --- a/spec/migrations/migrate_user_project_view_spec.rb +++ b/spec/migrations/migrate_user_project_view_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb') -describe MigrateUserProjectView do +describe MigrateUserProjectView, :truncate do let(:migration) { described_class.new } let!(:user) { create(:user) } diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb index 36e82729c23..4bd8d4ac0d1 100644 --- a/spec/migrations/rename_more_reserved_project_names_spec.rb +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -17,7 +17,9 @@ describe RenameMoreReservedProjectNames, truncate: true do describe '#up' do context 'when project repository exists' do - before { project.create_repository } + before do + project.create_repository + end context 'when no exception is raised' do it 'renames project with reserved names' do diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb index 4fb7ed36884..05e021c2e32 100644 --- a/spec/migrations/rename_reserved_project_names_spec.rb +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -17,7 +17,9 @@ describe RenameReservedProjectNames, truncate: true do describe '#up' do context 'when project repository exists' do - before { project.create_repository } + before do + project.create_repository + end context 'when no exception is raised' do it 'renames project with reserved names' do diff --git a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb index 175bf1876b2..42109fd0743 100644 --- a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb +++ b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb @@ -31,8 +31,8 @@ describe TurnNestedGroupsIntoRegularGroupsForMysql do end it 'adds members of parent groups as members to the migrated group' do - is_member = child_group.members. - where(user_id: member, access_level: Gitlab::Access::DEVELOPER).any? + is_member = child_group.members + .where(user_id: member, access_level: Gitlab::Access::DEVELOPER).any? expect(is_member).to eq(true) end @@ -44,21 +44,21 @@ describe TurnNestedGroupsIntoRegularGroupsForMysql do end it 'renames projects of the nested group' do - expect(updated_project.path_with_namespace). - to eq("#{parent_group.name}-#{child_group.name}/#{updated_project.path}") + expect(updated_project.path_with_namespace) + .to eq("#{parent_group.name}-#{child_group.name}/#{updated_project.path}") end it 'renames the repository of any projects' do - expect(updated_project.repository.path). - to end_with("#{parent_group.name}-#{child_group.name}/#{updated_project.path}.git") + expect(updated_project.repository.path) + .to end_with("#{parent_group.name}-#{child_group.name}/#{updated_project.path}.git") expect(File.directory?(updated_project.repository.path)).to eq(true) end it 'creates a redirect route for renamed projects' do - exists = RedirectRoute. - where(source_type: 'Project', source_id: project.id). - any? + exists = RedirectRoute + .where(source_type: 'Project', source_id: project.id) + .any? expect(exists).to eq(true) end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 92d70cfc64c..090f9e70c50 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -69,8 +69,8 @@ describe Ability, lib: true do project = create(:empty_project, :public) user = build(:user) - expect(described_class.users_that_can_read_project([user], project)). - to eq([user]) + expect(described_class.users_that_can_read_project([user], project)) + .to eq([user]) end end @@ -80,8 +80,8 @@ describe Ability, lib: true do it 'returns users that are administrators' do user = build(:user, admin: true) - expect(described_class.users_that_can_read_project([user], project)). - to eq([user]) + expect(described_class.users_that_can_read_project([user], project)) + .to eq([user]) end it 'returns internal users while skipping external users' do @@ -89,8 +89,8 @@ describe Ability, lib: true do user2 = build(:user, external: true) users = [user1, user2] - expect(described_class.users_that_can_read_project(users, project)). - to eq([user1]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([user1]) end it 'returns external users if they are the project owner' do @@ -100,8 +100,8 @@ describe Ability, lib: true do expect(project).to receive(:owner).twice.and_return(user1) - expect(described_class.users_that_can_read_project(users, project)). - to eq([user1]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([user1]) end it 'returns external users if they are project members' do @@ -111,8 +111,8 @@ describe Ability, lib: true do expect(project.team).to receive(:members).twice.and_return([user1]) - expect(described_class.users_that_can_read_project(users, project)). - to eq([user1]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([user1]) end it 'returns an empty Array if all users are external users without access' do @@ -120,8 +120,8 @@ describe Ability, lib: true do user2 = build(:user, external: true) users = [user1, user2] - expect(described_class.users_that_can_read_project(users, project)). - to eq([]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([]) end end @@ -131,8 +131,8 @@ describe Ability, lib: true do it 'returns users that are administrators' do user = build(:user, admin: true) - expect(described_class.users_that_can_read_project([user], project)). - to eq([user]) + expect(described_class.users_that_can_read_project([user], project)) + .to eq([user]) end it 'returns external users if they are the project owner' do @@ -142,8 +142,8 @@ describe Ability, lib: true do expect(project).to receive(:owner).twice.and_return(user1) - expect(described_class.users_that_can_read_project(users, project)). - to eq([user1]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([user1]) end it 'returns external users if they are project members' do @@ -153,8 +153,8 @@ describe Ability, lib: true do expect(project.team).to receive(:members).twice.and_return([user1]) - expect(described_class.users_that_can_read_project(users, project)). - to eq([user1]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([user1]) end it 'returns an empty Array if all users are internal users without access' do @@ -162,8 +162,8 @@ describe Ability, lib: true do user2 = build(:user) users = [user1, user2] - expect(described_class.users_that_can_read_project(users, project)). - to eq([]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([]) end it 'returns an empty Array if all users are external users without access' do @@ -171,8 +171,8 @@ describe Ability, lib: true do user2 = build(:user, external: true) users = [user1, user2] - expect(described_class.users_that_can_read_project(users, project)). - to eq([]) + expect(described_class.users_that_can_read_project(users, project)) + .to eq([]) end end end @@ -210,8 +210,8 @@ describe Ability, lib: true do user = build(:user, admin: true) issue = build(:issue) - expect(described_class.issues_readable_by_user([issue], user)). - to eq([issue]) + expect(described_class.issues_readable_by_user([issue], user)) + .to eq([issue]) end end @@ -222,8 +222,8 @@ describe Ability, lib: true do expect(issue).to receive(:readable_by?).with(user).and_return(true) - expect(described_class.issues_readable_by_user([issue], user)). - to eq([issue]) + expect(described_class.issues_readable_by_user([issue], user)) + .to eq([issue]) end it 'returns an empty Array when no issues are readable' do @@ -244,8 +244,8 @@ describe Ability, lib: true do expect(hidden_issue).to receive(:publicly_visible?).and_return(false) expect(visible_issue).to receive(:publicly_visible?).and_return(true) - issues = described_class. - issues_readable_by_user([hidden_issue, visible_issue]) + issues = described_class + .issues_readable_by_user([hidden_issue, visible_issue]) expect(issues).to eq([visible_issue]) end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index 90aec2b45e6..c1bf5551fe0 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -36,8 +36,8 @@ RSpec.describe AbuseReport, type: :model do describe '#notify' do it 'delivers' do - expect(AbuseReportMailer).to receive(:notify).with(subject.id). - and_return(spy) + expect(AbuseReportMailer).to receive(:notify).with(subject.id) + .and_return(spy) subject.notify end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index fa229542f70..166a4474abf 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -78,7 +78,9 @@ describe ApplicationSetting, models: true do # Upgraded databases will have this sort of content context 'repository_storages is a String, not an Array' do - before { setting.__send__(:raw_write_attribute, :repository_storages, 'default') } + before do + setting.__send__(:raw_write_attribute, :repository_storages, 'default') + end it { expect(setting.repository_storages_before_type_cast).to eq('default') } it { expect(setting.repository_storages).to eq(['default']) } diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 219db365a91..333f4139a96 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -21,22 +21,29 @@ describe BroadcastMessage, models: true do end describe '.current' do - it "returns last message if time match" do + it 'returns message if time match' do message = create(:broadcast_message) - expect(BroadcastMessage.current).to eq message + expect(BroadcastMessage.current).to include(message) end - it "returns nil if time not come" do + it 'returns multiple messages if time match' do + message1 = create(:broadcast_message) + message2 = create(:broadcast_message) + + expect(BroadcastMessage.current).to contain_exactly(message1, message2) + end + + it 'returns empty list if time not come' do create(:broadcast_message, :future) - expect(BroadcastMessage.current).to be_nil + expect(BroadcastMessage.current).to be_empty end - it "returns nil if time has passed" do + it 'returns empty list if time has passed' do create(:broadcast_message, :expired) - expect(BroadcastMessage.current).to be_nil + expect(BroadcastMessage.current).to be_empty end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index b0716e04d3d..488697f74eb 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -21,6 +21,18 @@ describe Ci::Build, :models do it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:trace) } + describe '.manual_actions' do + let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) } + let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) } + let!(:manual_action) { create(:ci_build, :manual, pipeline: pipeline) } + + subject { described_class.manual_actions } + + it { is_expected.to include(manual_action) } + it { is_expected.to include(manual_but_succeeded) } + it { is_expected.not_to include(manual_but_created) } + end + describe '#actionize' do context 'when build is a created' do before do @@ -95,12 +107,18 @@ describe Ci::Build, :models do it { is_expected.to be_truthy } context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } + before do + build.update(artifacts_expire_at: Time.now - 7.days) + end + it { is_expected.to be_falsy } end context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } + before do + build.update(artifacts_expire_at: Time.now + 7.days) + end + it { is_expected.to be_truthy } end end @@ -110,13 +128,17 @@ describe Ci::Build, :models do subject { build.artifacts_expired? } context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } + before do + build.update(artifacts_expire_at: Time.now - 7.days) + end it { is_expected.to be_truthy } end context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } + before do + build.update(artifacts_expire_at: Time.now + 7.days) + end it { is_expected.to be_falsey } end @@ -141,7 +163,9 @@ describe Ci::Build, :models do context 'when artifacts_expire_at is specified' do let(:expire_at) { Time.now + 7.days } - before { build.artifacts_expire_at = expire_at } + before do + build.artifacts_expire_at = expire_at + end it { is_expected.to be_within(5).of(expire_at - Time.now) } end @@ -427,42 +451,6 @@ describe Ci::Build, :models do end end - describe '#environment_url' do - subject { job.environment_url } - - context 'when yaml environment uses $CI_COMMIT_REF_NAME' do - let(:job) do - create(:ci_build, - ref: 'master', - options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } }) - end - - it { is_expected.to eq('http://review/master') } - end - - context 'when yaml environment uses yaml_variables containing symbol keys' do - let(:job) do - create(:ci_build, - yaml_variables: [{ key: :APP_HOST, value: 'host' }], - options: { environment: { url: 'http://review/$APP_HOST' } }) - end - - it { is_expected.to eq('http://review/host') } - end - - context 'when yaml environment does not have url' do - let(:job) { create(:ci_build, environment: 'staging') } - - let!(:environment) do - create(:environment, project: job.project, name: job.environment) - end - - it 'returns the external_url from persisted environment' do - is_expected.to eq(environment.external_url) - end - end - end - describe '#starts_environment?' do subject { build.starts_environment? } @@ -875,8 +863,8 @@ describe Ci::Build, :models do pipeline2 = create(:ci_pipeline, project: project) @build2 = create(:ci_build, pipeline: pipeline2) - allow(@merge_request).to receive(:commits_sha). - and_return([pipeline.sha, pipeline2.sha]) + allow(@merge_request).to receive(:commits_sha) + .and_return([pipeline.sha, pipeline2.sha]) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end @@ -926,6 +914,10 @@ describe Ci::Build, :models do context 'when other build is retried' do let!(:retried_build) { Ci::Build.retry(other_build, user) } + before do + retried_build.success + end + it 'returns a retried build' do is_expected.to contain_exactly(retried_build) end @@ -1071,7 +1063,9 @@ describe Ci::Build, :models do describe '#has_expiring_artifacts?' do context 'when artifacts have expiration date set' do - before { build.update(artifacts_expire_at: 1.day.from_now) } + before do + build.update(artifacts_expire_at: 1.day.from_now) + end it 'has expiring artifacts' do expect(build).to have_expiring_artifacts @@ -1079,7 +1073,9 @@ describe Ci::Build, :models do end context 'when artifacts do not have expiration date set' do - before { build.update(artifacts_expire_at: nil) } + before do + build.update(artifacts_expire_at: nil) + end it 'does not have expiring artifacts' do expect(build).not_to have_expiring_artifacts @@ -1260,10 +1256,20 @@ describe Ci::Build, :models do context 'when the URL was set from the job' do before do - build.update(options: { environment: { url: 'http://host/$CI_JOB_NAME' } }) + build.update(options: { environment: { url: url } }) end it_behaves_like 'containing environment variables' + + context 'when variables are used in the URL, it does not expand' do + let(:url) { 'http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG' } + + it_behaves_like 'containing environment variables' + + it 'puts $CI_ENVIRONMENT_URL in the last so all other variables are available to be used when runners are trying to expand it' do + expect(subject.last).to eq(environment_variables.last) + end + end end context 'when the URL was not set from the job, but environment' do diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index b00e7a73571..56817baf79d 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -40,8 +40,8 @@ describe Ci::PipelineSchedule, models: true do context 'when creates new pipeline schedule' do let(:expected_next_run_at) do - Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone). - next_time_from(Time.now) + Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone) + .next_time_from(Time.now) end it 'updates next_run_at automatically' do @@ -53,8 +53,8 @@ describe Ci::PipelineSchedule, models: true do let(:new_cron) { '0 0 1 1 *' } let(:expected_next_run_at) do - Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone). - next_time_from(Time.now) + Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone) + .next_time_from(Time.now) end it 'updates next_run_at automatically' do @@ -72,8 +72,8 @@ describe Ci::PipelineSchedule, models: true do let(:future_time) { 10.days.from_now } let(:expected_next_run_at) do - Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone). - next_time_from(future_time) + Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone) + .next_time_from(future_time) end it 'points to proper next_run_at' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b50c7700bd3..dab8e8ca432 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -608,8 +608,8 @@ describe Ci::Pipeline, models: true do it 'returns the latest pipeline for the same ref and different sha' do expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') - expect(pipelines.map(&:status)). - to contain_exactly('success', 'failed', 'skipped') + expect(pipelines.map(&:status)) + .to contain_exactly('success', 'failed', 'skipped') end end @@ -618,8 +618,8 @@ describe Ci::Pipeline, models: true do it 'returns the latest pipeline for ref and different sha' do expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') - expect(pipelines.map(&:status)). - to contain_exactly('success', 'failed') + expect(pipelines.map(&:status)) + .to contain_exactly('success', 'failed') end end end @@ -654,8 +654,8 @@ describe Ci::Pipeline, models: true do end it 'returns the latest successful pipeline' do - expect(described_class.latest_successful_for('ref')). - to eq(latest_successful_pipeline) + expect(described_class.latest_successful_for('ref')) + .to eq(latest_successful_pipeline) end end @@ -1156,7 +1156,9 @@ describe Ci::Pipeline, models: true do end context 'when pipeline is not stuck' do - before { create(:ci_runner, :shared, :online) } + before do + create(:ci_runner, :shared, :online) + end it 'is not stuck' do expect(pipeline).not_to be_stuck @@ -1199,8 +1201,8 @@ describe Ci::Pipeline, models: true do before do project.team << [pipeline.user, Gitlab::Access::DEVELOPER] - pipeline.user.global_notification_setting. - update(level: 'custom', failed_pipeline: true, success_pipeline: true) + pipeline.user.global_notification_setting + .update(level: 'custom', failed_pipeline: true, success_pipeline: true) reset_delivered_emails! diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 077b10227d7..83494af24ba 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -54,8 +54,8 @@ describe Ci::Variable, models: true do it 'fails to decrypt if iv is incorrect' do subject.encrypted_value_iv = SecureRandom.hex subject.instance_variable_set(:@value, nil) - expect { subject.value }. - to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') + expect { subject.value } + .to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') end end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index e4bddf67096..ba9c3f66d21 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -147,9 +147,9 @@ describe CommitRange, models: true do note: commit1.revert_description(user), project: issue.project) - expect_any_instance_of(Commit).to receive(:reverts_commit?). - with(commit1, user). - and_return(true) + expect_any_instance_of(Commit).to receive(:reverts_commit?) + .with(commit1, user) + .and_return(true) expect(commit1.has_been_reverted?(user, issue)).to eq(true) end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ba247dcc5cf..6056d78da4e 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -206,19 +206,25 @@ eos it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } context 'commit has no description' do - before { allow(commit).to receive(:description?).and_return(false) } + before do + allow(commit).to receive(:description?).and_return(false) + end it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description does not revert commit" do - before { allow(commit).to receive(:description).and_return("Foo Bar") } + before do + allow(commit).to receive(:description).and_return("Foo Bar") + end it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description reverts commit" do - before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") } + before do + allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") + end it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index c50b8bf7b13..1e074c7ad26 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -31,7 +31,10 @@ describe CommitStatus, :models do describe '#author' do subject { commit_status.author } - before { commit_status.author = User.new } + + before do + commit_status.author = User.new + end it { is_expected.to eq(commit_status.user) } end @@ -50,14 +53,18 @@ describe CommitStatus, :models do subject { commit_status.started? } context 'without started_at' do - before { commit_status.started_at = nil } + before do + commit_status.started_at = nil + end it { is_expected.to be_falsey } end %w[running success failed].each do |status| context "if commit status is #{status}" do - before { commit_status.status = status } + before do + commit_status.status = status + end it { is_expected.to be_truthy } end @@ -65,7 +72,9 @@ describe CommitStatus, :models do %w[pending canceled].each do |status| context "if commit status is #{status}" do - before { commit_status.status = status } + before do + commit_status.status = status + end it { is_expected.to be_falsey } end @@ -77,7 +86,9 @@ describe CommitStatus, :models do %w[pending running].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_truthy } end @@ -85,7 +96,9 @@ describe CommitStatus, :models do %w[success failed canceled].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_falsey } end @@ -97,7 +110,9 @@ describe CommitStatus, :models do %w[success failed canceled].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_truthy } end @@ -105,7 +120,9 @@ describe CommitStatus, :models do %w[pending running].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_falsey } end @@ -267,11 +284,48 @@ describe CommitStatus, :models do end end + describe '.status' do + context 'when there are multiple statuses present' do + before do + create_status(status: 'running') + create_status(status: 'success') + create_status(allow_failure: true, status: 'failed') + end + + it 'returns a correct compound status' do + expect(described_class.all.status).to eq 'running' + end + end + + context 'when there are only allowed to fail commit statuses present' do + before do + create_status(allow_failure: true, status: 'failed') + end + + it 'returns status that indicates success' do + expect(described_class.all.status).to eq 'success' + end + end + + context 'when using a scope to select latest statuses' do + before do + create_status(name: 'test', retried: true, status: 'failed') + create_status(allow_failure: true, name: 'test', status: 'failed') + end + + it 'returns status according to the scope' do + expect(described_class.latest.status).to eq 'success' + end + end + end + describe '#before_sha' do subject { commit_status.before_sha } context 'when no before_sha is set for pipeline' do - before { pipeline.before_sha = nil } + before do + pipeline.before_sha = nil + end it 'returns blank sha' do is_expected.to eq(Gitlab::Git::BLANK_SHA) @@ -280,7 +334,10 @@ describe CommitStatus, :models do context 'for before_sha set for pipeline' do let(:value) { '1234' } - before { pipeline.before_sha = value } + + before do + pipeline.before_sha = value + end it 'returns the set value' do is_expected.to eq(value) diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb index 4829ef17a20..97b7e48bb3c 100644 --- a/spec/models/concerns/access_requestable_spec.rb +++ b/spec/models/concerns/access_requestable_spec.rb @@ -14,7 +14,9 @@ describe AccessRequestable do let(:group) { create(:group, :public, :access_requestable) } let(:user) { create(:user) } - before { group.request_access(user) } + before do + group.request_access(user) + end it { expect(group.requesters.exists?(user_id: user)).to be_truthy } end @@ -32,7 +34,9 @@ describe AccessRequestable do let(:project) { create(:empty_project, :public, :access_requestable) } let(:user) { create(:user) } - before { project.request_access(user) } + before do + project.request_access(user) + end it { expect(project.requesters.exists?(user_id: user)).to be_truthy } end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 92fdc5cd65d..a6fccb668e3 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -15,13 +15,13 @@ describe CaseSensitivity, models: true do it 'returns the criteria for a column and a value' do criteria = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:foo). - and_return('"foo"') + expect(connection).to receive(:quote_table_name) + .with(:foo) + .and_return('"foo"') - expect(model).to receive(:where). - with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). - and_return(criteria) + expect(model).to receive(:where) + .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar') + .and_return(criteria) expect(model.iwhere(foo: 'bar')).to eq(criteria) end @@ -29,13 +29,13 @@ describe CaseSensitivity, models: true do it 'returns the criteria for a column with a table, and a value' do criteria = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:'foo.bar'). - and_return('"foo"."bar"') + expect(connection).to receive(:quote_table_name) + .with(:'foo.bar') + .and_return('"foo"."bar"') - expect(model).to receive(:where). - with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). - and_return(criteria) + expect(model).to receive(:where) + .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar') + .and_return(criteria) expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria) end @@ -46,21 +46,21 @@ describe CaseSensitivity, models: true do initial = double(:criteria) final = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:foo). - and_return('"foo"') + expect(connection).to receive(:quote_table_name) + .with(:foo) + .and_return('"foo"') - expect(connection).to receive(:quote_table_name). - with(:bar). - and_return('"bar"') + expect(connection).to receive(:quote_table_name) + .with(:bar) + .and_return('"bar"') - expect(model).to receive(:where). - with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). - and_return(initial) + expect(model).to receive(:where) + .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar') + .and_return(initial) - expect(initial).to receive(:where). - with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz'). - and_return(final) + expect(initial).to receive(:where) + .with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz') + .and_return(final) got = model.iwhere(foo: 'bar', bar: 'baz') @@ -71,21 +71,21 @@ describe CaseSensitivity, models: true do initial = double(:criteria) final = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:'foo.bar'). - and_return('"foo"."bar"') + expect(connection).to receive(:quote_table_name) + .with(:'foo.bar') + .and_return('"foo"."bar"') - expect(connection).to receive(:quote_table_name). - with(:'foo.baz'). - and_return('"foo"."baz"') + expect(connection).to receive(:quote_table_name) + .with(:'foo.baz') + .and_return('"foo"."baz"') - expect(model).to receive(:where). - with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). - and_return(initial) + expect(model).to receive(:where) + .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar') + .and_return(initial) - expect(initial).to receive(:where). - with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). - and_return(final) + expect(initial).to receive(:where) + .with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz') + .and_return(final) got = model.iwhere('foo.bar'.to_sym => 'bar', 'foo.baz'.to_sym => 'baz') @@ -105,13 +105,13 @@ describe CaseSensitivity, models: true do it 'returns the criteria for a column and a value' do criteria = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:foo). - and_return('`foo`') + expect(connection).to receive(:quote_table_name) + .with(:foo) + .and_return('`foo`') - expect(model).to receive(:where). - with(%q{`foo` = :value}, value: 'bar'). - and_return(criteria) + expect(model).to receive(:where) + .with(%q{`foo` = :value}, value: 'bar') + .and_return(criteria) expect(model.iwhere(foo: 'bar')).to eq(criteria) end @@ -119,16 +119,16 @@ describe CaseSensitivity, models: true do it 'returns the criteria for a column with a table, and a value' do criteria = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:'foo.bar'). - and_return('`foo`.`bar`') + expect(connection).to receive(:quote_table_name) + .with(:'foo.bar') + .and_return('`foo`.`bar`') - expect(model).to receive(:where). - with(%q{`foo`.`bar` = :value}, value: 'bar'). - and_return(criteria) + expect(model).to receive(:where) + .with(%q{`foo`.`bar` = :value}, value: 'bar') + .and_return(criteria) - expect(model.iwhere('foo.bar'.to_sym => 'bar')). - to eq(criteria) + expect(model.iwhere('foo.bar'.to_sym => 'bar')) + .to eq(criteria) end end @@ -137,21 +137,21 @@ describe CaseSensitivity, models: true do initial = double(:criteria) final = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:foo). - and_return('`foo`') + expect(connection).to receive(:quote_table_name) + .with(:foo) + .and_return('`foo`') - expect(connection).to receive(:quote_table_name). - with(:bar). - and_return('`bar`') + expect(connection).to receive(:quote_table_name) + .with(:bar) + .and_return('`bar`') - expect(model).to receive(:where). - with(%q{`foo` = :value}, value: 'bar'). - and_return(initial) + expect(model).to receive(:where) + .with(%q{`foo` = :value}, value: 'bar') + .and_return(initial) - expect(initial).to receive(:where). - with(%q{`bar` = :value}, value: 'baz'). - and_return(final) + expect(initial).to receive(:where) + .with(%q{`bar` = :value}, value: 'baz') + .and_return(final) got = model.iwhere(foo: 'bar', bar: 'baz') @@ -162,21 +162,21 @@ describe CaseSensitivity, models: true do initial = double(:criteria) final = double(:criteria) - expect(connection).to receive(:quote_table_name). - with(:'foo.bar'). - and_return('`foo`.`bar`') + expect(connection).to receive(:quote_table_name) + .with(:'foo.bar') + .and_return('`foo`.`bar`') - expect(connection).to receive(:quote_table_name). - with(:'foo.baz'). - and_return('`foo`.`baz`') + expect(connection).to receive(:quote_table_name) + .with(:'foo.baz') + .and_return('`foo`.`baz`') - expect(model).to receive(:where). - with(%q{`foo`.`bar` = :value}, value: 'bar'). - and_return(initial) + expect(model).to receive(:where) + .with(%q{`foo`.`bar` = :value}, value: 'bar') + .and_return(initial) - expect(initial).to receive(:where). - with(%q{`foo`.`baz` = :value}, value: 'baz'). - and_return(final) + expect(initial).to receive(:where) + .with(%q{`foo`.`baz` = :value}, value: 'baz') + .and_return(final) got = model.iwhere('foo.bar'.to_sym => 'bar', 'foo.baz'.to_sym => 'baz') diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 67dae7cf4c0..a38f2553eb1 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -48,7 +48,7 @@ describe HasStatus do [create(type, status: :failed, allow_failure: true)] end - it { is_expected.to eq 'skipped' } + it { is_expected.to eq 'success' } end context 'success and canceled' do @@ -168,8 +168,8 @@ describe HasStatus do describe ".#{status}" do it 'contains the job' do - expect(CommitStatus.public_send(status).all). - to contain_exactly(job) + expect(CommitStatus.public_send(status).all) + .to contain_exactly(job) end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 27890e33b49..ac9303370ab 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -69,8 +69,8 @@ describe Issuable do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } it 'returns notes with a matching title' do - expect(issuable_class.search(searchable_issue.title)). - to eq([searchable_issue]) + expect(issuable_class.search(searchable_issue.title)) + .to eq([searchable_issue]) end it 'returns notes with a partially matching title' do @@ -78,8 +78,8 @@ describe Issuable do end it 'returns notes with a matching title regardless of the casing' do - expect(issuable_class.search(searchable_issue.title.upcase)). - to eq([searchable_issue]) + expect(issuable_class.search(searchable_issue.title.upcase)) + .to eq([searchable_issue]) end end @@ -89,8 +89,8 @@ describe Issuable do end it 'returns notes with a matching title' do - expect(issuable_class.full_search(searchable_issue.title)). - to eq([searchable_issue]) + expect(issuable_class.full_search(searchable_issue.title)) + .to eq([searchable_issue]) end it 'returns notes with a partially matching title' do @@ -98,23 +98,23 @@ describe Issuable do end it 'returns notes with a matching title regardless of the casing' do - expect(issuable_class.full_search(searchable_issue.title.upcase)). - to eq([searchable_issue]) + expect(issuable_class.full_search(searchable_issue.title.upcase)) + .to eq([searchable_issue]) end it 'returns notes with a matching description' do - expect(issuable_class.full_search(searchable_issue.description)). - to eq([searchable_issue]) + expect(issuable_class.full_search(searchable_issue.description)) + .to eq([searchable_issue]) end it 'returns notes with a partially matching description' do - expect(issuable_class.full_search(searchable_issue.description)). - to eq([searchable_issue]) + expect(issuable_class.full_search(searchable_issue.description)) + .to eq([searchable_issue]) end it 'returns notes with a matching description regardless of the casing' do - expect(issuable_class.full_search(searchable_issue.description.upcase)). - to eq([searchable_issue]) + expect(issuable_class.full_search(searchable_issue.description.upcase)) + .to eq([searchable_issue]) end end @@ -200,7 +200,9 @@ describe Issuable do let(:project) { issue.project } context 'user is not a participant in the issue' do - before { allow(issue).to receive(:participants).with(user).and_return([]) } + before do + allow(issue).to receive(:participants).with(user).and_return([]) + end it 'returns false when no subcription exists' do expect(issue.subscribed?(user, project)).to be_falsey @@ -220,7 +222,9 @@ describe Issuable do end context 'user is a participant in the issue' do - before { allow(issue).to receive(:participants).with(user).and_return([user]) } + before do + allow(issue).to receive(:participants).with(user).and_return([user]) + end it 'returns false when no subcription exists' do expect(issue.subscribed?(user, project)).to be_truthy @@ -252,7 +256,9 @@ describe Issuable do end context "issue is assigned" do - before { issue.assignees << user } + before do + issue.assignees << user + end it "returns correct hook data" do expect(data[:assignees].first).to eq(user.hook_attrs) @@ -276,7 +282,9 @@ describe Issuable do context 'issue has labels' do let(:labels) { [create(:label), create(:label)] } - before { issue.update_attribute(:labels, labels)} + before do + issue.update_attribute(:labels, labels) + end it 'includes labels in the hook data' do expect(data[:labels]).to eq(labels.map(&:hook_attrs)) diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index e382c7120de..e2a29e0ae70 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -61,7 +61,9 @@ describe Issue, "Mentionable" do end context 'when the current user can see the issue' do - before { private_project.team << [user, Gitlab::Access::DEVELOPER] } + before do + private_project.team << [user, Gitlab::Access::DEVELOPER] + end it 'includes the reference' do expect(referenced_issues(user)).to contain_exactly(private_issue, public_issue) diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 675b730c557..cefe7fb6fea 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -19,12 +19,43 @@ describe Milestone, 'Milestoneish' do let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) } let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignees: [assignee], milestone: milestone) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } + let(:label_3) { create(:label, title: 'label_3', project: project) } before do project.team << [member, :developer] project.team << [guest, :guest] end + describe '#sorted_issues' do + it 'sorts issues by label priority' do + issue.labels << label_1 + security_issue_1.labels << label_2 + closed_issue_1.labels << label_3 + + issues = milestone.sorted_issues(member) + + expect(issues.first).to eq(issue) + expect(issues.second).to eq(security_issue_1) + expect(issues.third).not_to eq(closed_issue_1) + end + end + + describe '#sorted_merge_requests' do + it 'sorts merge requests by label priority' do + merge_request_1 = create(:labeled_merge_request, labels: [label_2], source_project: project, source_branch: 'branch_1', milestone: milestone) + merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone) + merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone) + + merge_requests = milestone.sorted_merge_requests + + expect(merge_requests.first).to eq(merge_request_2) + expect(merge_requests.second).to eq(merge_request_1) + expect(merge_requests.third).to eq(merge_request_3) + end + end + describe '#closed_items_count' do it 'does not count confidential issues for non project members' do expect(milestone.closed_items_count(non_member)).to eq 2 diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index a0765a264cf..808247ebfd5 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -40,7 +40,10 @@ describe ReactiveCaching, caching: true do let(:instance) { CacheTest.new(666, &calculation) } describe '#with_reactive_cache' do - before { stub_reactive_cache } + before do + stub_reactive_cache + end + subject(:go!) { instance.result } context 'when cache is empty' do @@ -60,12 +63,17 @@ describe ReactiveCaching, caching: true do end context 'when the cache is full' do - before { stub_reactive_cache(instance, 4) } + before do + stub_reactive_cache(instance, 4) + end it { is_expected.to eq(2) } context 'and expired' do - before { invalidate_reactive_cache(instance) } + before do + invalidate_reactive_cache(instance) + end + it { is_expected.to be_nil } end end @@ -84,7 +92,9 @@ describe ReactiveCaching, caching: true do subject(:go!) { instance.exclusively_update_reactive_cache! } context 'when the lease is free and lifetime is not exceeded' do - before { stub_reactive_cache(instance, "preexisting") } + before do + stub_reactive_cache(instance, "preexisting") + end it 'takes and releases the lease' do expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000") @@ -106,7 +116,10 @@ describe ReactiveCaching, caching: true do end context 'and #calculate_reactive_cache raises an exception' do - before { stub_reactive_cache(instance, "preexisting") } + before do + stub_reactive_cache(instance, "preexisting") + end + let(:calculation) { -> { raise "foo"} } it 'leaves the cache untouched' do diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb index 18327fe262d..3934992c143 100644 --- a/spec/models/concerns/resolvable_discussion_spec.rb +++ b/spec/models/concerns/resolvable_discussion_spec.rb @@ -306,22 +306,22 @@ describe Discussion, ResolvableDiscussion, models: true do it "doesn't change resolved_at on the resolved note" do expect(first_note.resolved_at).not_to be_nil - expect { subject.resolve!(current_user) }. - not_to change { first_note.reload.resolved_at } + expect { subject.resolve!(current_user) } + .not_to change { first_note.reload.resolved_at } end it "doesn't change resolved_by on the resolved note" do expect(first_note.resolved_by).to eq(user) - expect { subject.resolve!(current_user) }. - not_to change { first_note.reload && first_note.resolved_by } + expect { subject.resolve!(current_user) } + .not_to change { first_note.reload && first_note.resolved_by } end it "doesn't change the resolved state on the resolved note" do expect(first_note.resolved?).to be true - expect { subject.resolve!(current_user) }. - not_to change { first_note.reload && first_note.resolved? } + expect { subject.resolve!(current_user) } + .not_to change { first_note.reload && first_note.resolved? } end it "sets resolved_at on the unresolved note" do diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 4b0bfa43abf..882afeccfc6 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -49,7 +49,10 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end context 'token is generated' do - before { subject.send("reset_#{token_field}!") } + before do + subject.send("reset_#{token_field}!") + end + it 'persists a new token' do expect(subject.send(:read_attribute, token_field)).to be_a String end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 6f0d2db23c7..bb84d3fc13d 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -30,7 +30,7 @@ describe Deployment, models: true do end describe '#includes_commit?' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository) } let(:environment) { create(:environment, project: project) } let(:deployment) do create(:deployment, environment: environment, sha: project.commit.id) @@ -90,6 +90,36 @@ describe Deployment, models: true do end end + describe '#additional_metrics' do + let(:project) { create(:project) } + let(:deployment) { create(:deployment, project: project) } + + subject { deployment.additional_metrics } + + context 'metrics are disabled' do + it { is_expected.to eq({}) } + end + + context 'metrics are enabled' do + let(:simple_metrics) do + { + success: true, + metrics: {}, + last_update: 42 + } + end + + let(:prometheus_service) { double('prometheus_service') } + + before do + allow(project).to receive(:prometheus_service).and_return(prometheus_service) + allow(prometheus_service).to receive(:additional_deployment_metrics).and_return(simple_metrics) + end + + it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) } + end + end + describe '#stop_action' do let(:build) { create(:ci_build) } @@ -102,7 +132,7 @@ describe Deployment, models: true do end context 'with other actions' do - let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } context 'when matching action is defined' do let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') } @@ -130,7 +160,7 @@ describe Deployment, models: true do context 'when matching action is defined' do let(:build) { create(:ci_build) } let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') } - let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } it { is_expected.to be_truthy } end diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb new file mode 100644 index 00000000000..3755f4a56f3 --- /dev/null +++ b/spec/models/diff_viewer/base_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' + +describe DiffViewer::Base, model: true do + include FakeBlobHelpers + + let(:project) { create(:project, :repository) } + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + let(:viewer_class) do + Class.new(described_class) do + include DiffViewer::ServerSide + + self.extensions = %w(jpg) + self.binary = true + self.collapse_limit = 1.megabyte + self.size_limit = 5.megabytes + end + end + + let(:viewer) { viewer_class.new(diff_file) } + + describe '.can_render?' do + context 'when the extension is supported' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + context 'when the binaryness matches' do + it 'returns true' do + expect(viewer_class.can_render?(diff_file)).to be_truthy + end + end + + context 'when the binaryness does not match' do + before do + allow(diff_file.old_blob).to receive(:binary?).and_return(false) + allow(diff_file.new_blob).to receive(:binary?).and_return(false) + end + + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + end + + context 'when the file type is supported' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('LICENSE') } + + before do + viewer_class.file_types = %i(license) + viewer_class.binary = false + end + + context 'when the binaryness matches' do + it 'returns true' do + expect(viewer_class.can_render?(diff_file)).to be_truthy + end + end + + context 'when the binaryness does not match' do + before do + allow(diff_file.old_blob).to receive(:binary?).and_return(true) + allow(diff_file.new_blob).to receive(:binary?).and_return(true) + end + + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + end + + context 'when the extension and file type are not supported' do + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + + context 'when the file was renamed and only the old blob is supported' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + before do + allow(diff_file).to receive(:renamed_file?).and_return(true) + allow(diff_file.new_blob).to receive(:extension).and_return('jpeg') + end + + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + end + + describe '#collapsed?' do + context 'when the combined blob size is larger than the collapse limit' do + before do + allow(diff_file.old_blob).to receive(:raw_size).and_return(512.kilobytes) + allow(diff_file.new_blob).to receive(:raw_size).and_return(513.kilobytes) + end + + it 'returns true' do + expect(viewer.collapsed?).to be_truthy + end + end + + context 'when the combined blob size is smaller than the collapse limit' do + it 'returns false' do + expect(viewer.collapsed?).to be_falsey + end + end + end + + describe '#too_large?' do + context 'when the combined blob size is larger than the size limit' do + before do + allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) + allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + end + + it 'returns true' do + expect(viewer.too_large?).to be_truthy + end + end + + context 'when the blob size is smaller than the size limit' do + it 'returns false' do + expect(viewer.too_large?).to be_falsey + end + end + end + + describe '#render_error' do + context 'when the combined blob size is larger than the size limit' do + before do + allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) + allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + end + + it 'returns :too_large' do + expect(viewer.render_error).to eq(:too_large) + end + end + + context 'when the combined blob size is smaller than the size limit' do + it 'returns nil' do + expect(viewer.render_error).to be_nil + end + end + end +end diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb new file mode 100644 index 00000000000..2d926e06936 --- /dev/null +++ b/spec/models/diff_viewer/server_side_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe DiffViewer::ServerSide, model: true do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + let(:viewer_class) do + Class.new(DiffViewer::Base) do + include DiffViewer::ServerSide + end + end + + subject { viewer_class.new(diff_file) } + + describe '#prepare!' do + it 'loads all diff file data' do + expect(diff_file.old_blob).to receive(:load_all_data!) + expect(diff_file.new_blob).to receive(:load_all_data!) + + subject.prepare! + end + end + + describe '#render_error' do + context 'when the diff file is stored externally' do + before do + allow(diff_file).to receive(:stored_externally?).and_return(true) + end + + it 'return :server_side_but_stored_externally' do + expect(subject.render_error).to eq(:server_side_but_stored_externally) + end + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index fe69c8e351d..b0635c6a90a 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -170,7 +170,7 @@ describe Environment, models: true do context 'when matching action is defined' do let(:build) { create(:ci_build) } let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } context 'when environment is available' do before do @@ -432,6 +432,99 @@ describe Environment, models: true do end end + describe '#has_metrics?' do + subject { environment.has_metrics? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:prometheus_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a monitoring service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:prometheus_project) } + + before do + environment.stop + end + + it { is_expected.to be_falsy } + end + end + + describe '#additional_metrics' do + let(:project) { create(:prometheus_project) } + subject { environment.additional_metrics } + + context 'when the environment has additional metrics' do + before do + allow(environment).to receive(:has_additional_metrics?).and_return(true) + end + + it 'returns the additional metrics from the deployment service' do + expect(project.prometheus_service).to receive(:additional_environment_metrics) + .with(environment) + .and_return(:fake_metrics) + + is_expected.to eq(:fake_metrics) + end + end + + context 'when the environment does not have metrics' do + before do + allow(environment).to receive(:has_additional_metrics?).and_return(false) + end + + it { is_expected.to be_nil } + end + end + + describe '#has_additional_metrics??' do + subject { environment.has_additional_metrics? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:prometheus_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a monitoring service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:prometheus_project) } + + before do + environment.stop + end + + it { is_expected.to be_falsy } + end + end + describe '#slug' do it "is automatically generated" do expect(environment.slug).not_to be_nil diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index b8cb967c4cc..10b9bf9f43a 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -266,8 +266,8 @@ describe Event, models: true do it 'does not update the project' do project.update(last_activity_at: Time.now) - expect(project).not_to receive(:update_column). - with(:last_activity_at, a_kind_of(Time)) + expect(project).not_to receive(:update_column) + .with(:last_activity_at, a_kind_of(Time)) create_push_event(project, project.owner) end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index f4c3e6d503f..152e97e09bf 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -19,7 +19,10 @@ describe GenericCommitStatus, models: true do describe '#context' do subject { generic_commit_status.context } - before { generic_commit_status.context = 'my_context' } + + before do + generic_commit_status.context = 'my_context' + end it { is_expected.to eq(generic_commit_status.name) } end @@ -39,7 +42,9 @@ describe GenericCommitStatus, models: true do end context 'when user has ability to see datails' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'details path points to an external URL' do expect(status).to have_details diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 3d437ca0fcc..4de1683b21c 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -143,14 +143,20 @@ describe Group, models: true do describe '#add_user' do let(:user) { create(:user) } - before { group.add_user(user, GroupMember::MASTER) } + + before do + group.add_user(user, GroupMember::MASTER) + end it { expect(group.group_members.masters.map(&:user)).to include(user) } end describe '#add_users' do let(:user) { create(:user) } - before { group.add_users([user.id], GroupMember::GUEST) } + + before do + group.add_users([user.id], GroupMember::GUEST) + end it "updates the group permission" do expect(group.group_members.guests.map(&:user)).to include(user) @@ -162,7 +168,10 @@ describe Group, models: true do describe '#avatar_type' do let(:user) { create(:user) } - before { group.add_user(user, GroupMember::MASTER) } + + before do + group.add_user(user, GroupMember::MASTER) + end it "is true if avatar is image" do group.update_attribute(:avatar, 'uploads/avatar.png') @@ -182,7 +191,9 @@ describe Group, models: true do let(:avatar_path) { "/uploads/system/group/avatar/#{group.id}/dk.png" } context 'when avatar file is uploaded' do - before { group.add_master(user) } + before do + group.add_master(user) + end it 'shows correct avatar url' do expect(group.avatar_url).to eq(avatar_path) @@ -222,7 +233,9 @@ describe Group, models: true do end describe '#has_owner?' do - before { @members = setup_group_members(group) } + before do + @members = setup_group_members(group) + end it { expect(group.has_owner?(@members[:owner])).to be_truthy } it { expect(group.has_owner?(@members[:master])).to be_falsey } @@ -233,7 +246,9 @@ describe Group, models: true do end describe '#has_master?' do - before { @members = setup_group_members(group) } + before do + @members = setup_group_members(group) + end it { expect(group.has_master?(@members[:owner])).to be_falsey } it { expect(group.has_master?(@members[:master])).to be_truthy } @@ -359,8 +374,8 @@ describe Group, models: true do group.add_user(master, GroupMember::MASTER) group.add_user(developer, GroupMember::DEVELOPER) - expect(group.user_ids_for_project_authorizations). - to include(master.id, developer.id) + expect(group.user_ids_for_project_authorizations) + .to include(master.id, developer.id) end end diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb index 93c2c538e10..04d23d4c4fd 100644 --- a/spec/models/issue_collection_spec.rb +++ b/spec/models/issue_collection_spec.rb @@ -50,8 +50,8 @@ describe IssueCollection do context 'using a user that is the owner of a project' do it 'returns the issues of the project' do - expect(collection.updatable_by_user(project.namespace.owner)). - to eq([issue1, issue2]) + expect(collection.updatable_by_user(project.namespace.owner)) + .to eq([issue1, issue2]) end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index bb4e70db2e9..bf97c6ececd 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -33,8 +33,8 @@ describe Issue, models: true do let!(:issue4) { create(:issue, project: project, relative_position: 200) } it 'returns ordered list' do - expect(project.issues.order_by_position_and_priority). - to match [issue3, issue4, issue1, issue2] + expect(project.issues.order_by_position_and_priority) + .to match [issue3, issue4, issue1, issue2] end end @@ -43,16 +43,16 @@ describe Issue, models: true do allow(subject).to receive(:author).and_return(double(name: 'Robert')) allow(subject).to receive(:assignees).and_return([]) - expect(subject.card_attributes). - to eq({ 'Author' => 'Robert', 'Assignee' => '' }) + expect(subject.card_attributes) + .to eq({ 'Author' => 'Robert', 'Assignee' => '' }) end it 'includes the assignee name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) allow(subject).to receive(:assignees).and_return([double(name: 'Douwe')]) - expect(subject.card_attributes). - to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) + expect(subject.card_attributes) + .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) end end @@ -245,7 +245,9 @@ describe Issue, models: true do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end it { is_expected.to eq true } @@ -259,12 +261,18 @@ describe Issue, models: true do let(:to_project) { create(:empty_project) } context 'destination project allowed' do - before { to_project.team << [user, :reporter] } + before do + to_project.team << [user, :reporter] + end + it { is_expected.to eq true } end context 'destination project not allowed' do - before { to_project.team << [user, :guest] } + before do + to_project.team << [user, :guest] + end + it { is_expected.to eq false } end end @@ -291,8 +299,8 @@ describe Issue, models: true do let(:user) { build(:admin) } before do - allow(subject.project.repository).to receive(:branch_names). - and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"]) + allow(subject.project.repository).to receive(:branch_names) + .and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"]) # Without this stub, the `create(:merge_request)` above fails because it can't find # the source branch. This seems like a reasonable compromise, in comparison with @@ -314,8 +322,8 @@ describe Issue, models: true do end it 'excludes stable branches from the related branches' do - allow(subject.project.repository).to receive(:branch_names). - and_return(["#{subject.iid}-0-stable"]) + allow(subject.project.repository).to receive(:branch_names) + .and_return(["#{subject.iid}-0-stable"]) expect(subject.related_branches(user)).to eq [] end @@ -549,7 +557,9 @@ describe Issue, models: true do end context 'when the user is the project owner' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'returns true for a regular issue' do issue = build(:issue, project: project) diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index f1e2a2cc518..f27920f9feb 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -34,8 +34,8 @@ describe Key, models: true do context 'when key was not updated during the last day' do before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). - and_return('000000') + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) + .and_return('000000') end it 'enqueues a UseKeyWorker job' do @@ -46,8 +46,8 @@ describe Key, models: true do context 'when key was updated during the last day' do before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). - and_return(false) + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) + .and_return(false) end it 'does not enqueue a UseKeyWorker job' do diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 84867e3d96b..31190fe5685 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -59,8 +59,8 @@ describe Label, models: true do describe '#text_color' do it 'uses default color if color is missing' do - expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR). - and_return(spy) + expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR) + .and_return(spy) label = described_class.new(color: nil) diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index ccc3deac199..494a88368ba 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -83,8 +83,8 @@ describe Member, models: true do @accepted_invite_member = create(:project_member, :developer, project: project, invite_token: '1234', - invite_email: 'toto2@example.com'). - tap { |u| u.accept_invite!(accepted_invite_user) } + invite_email: 'toto2@example.com') + .tap { |u| u.accept_invite!(accepted_invite_user) } requested_user = create(:user).tap { |u| project.request_access(u) } @requested_member = project.requesters.find_by(user_id: requested_user.id) @@ -265,8 +265,8 @@ describe Member, models: true do expect(source.users).not_to include(user) expect(source.requesters.exists?(user_id: user)).to be_truthy - expect { described_class.add_user(source, user, :master) }. - to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.add_user(source, user, :master) } + .to raise_error(Gitlab::Access::AccessDeniedError) expect(source.users.reload).not_to include(user) expect(source.requesters.reload.exists?(user_id: user)).to be_truthy diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 17765b25856..37014268a70 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -33,8 +33,8 @@ describe GroupMember, models: true do it "sends email to user" do membership = build(:group_member) - allow(membership).to receive(:notification_service). - and_return(double('NotificationService').as_null_object) + allow(membership).to receive(:notification_service) + .and_return(double('NotificationService').as_null_object) expect(membership).to receive(:notification_service) membership.save @@ -44,8 +44,8 @@ describe GroupMember, models: true do describe "#after_update" do before do @group_member = create :group_member - allow(@group_member).to receive(:notification_service). - and_return(double('NotificationService').as_null_object) + allow(@group_member).to receive(:notification_service) + .and_return(double('NotificationService').as_null_object) end it "sends email to user" do diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb new file mode 100644 index 00000000000..7276f5b5061 --- /dev/null +++ b/spec/models/merge_request_diff_file_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +describe MergeRequestDiffFile, type: :model do + describe '#utf8_diff' do + it 'does not raise error when a hash value is in binary' do + subject.diff = "\x05\x00\x68\x65\x6c\x6c\x6f" + + expect { subject.utf8_diff }.not_to raise_error + end + end +end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index ed9fde57bf7..4ad4abaa572 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -36,7 +36,9 @@ describe MergeRequestDiff, models: true do end context 'when the raw diffs are empty' do - before { mr_diff.update_attributes(st_diffs: '') } + before do + MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id) + end it 'returns an empty DiffCollection' do expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection) @@ -45,7 +47,10 @@ describe MergeRequestDiff, models: true do end context 'when the raw diffs have invalid content' do - before { mr_diff.update_attributes(st_diffs: ["--broken-diff"]) } + before do + MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id) + mr_diff.update_attributes(st_diffs: ["--broken-diff"]) + end it 'returns an empty DiffCollection' do expect(mr_diff.raw_diffs.to_a).to be_empty diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 060754fab63..bb5273074a2 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -92,16 +92,16 @@ describe MergeRequest, models: true do allow(subject).to receive(:author).and_return(double(name: 'Robert')) allow(subject).to receive(:assignee).and_return(nil) - expect(subject.card_attributes). - to eq({ 'Author' => 'Robert', 'Assignee' => nil }) + expect(subject.card_attributes) + .to eq({ 'Author' => 'Robert', 'Assignee' => nil }) end it 'includes the assignee name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) allow(subject).to receive(:assignee).and_return(double(name: 'Douwe')) - expect(subject.card_attributes). - to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) + expect(subject.card_attributes) + .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) end end @@ -361,8 +361,8 @@ describe MergeRequest, models: true do end it 'accesses the set of issues that will be closed on acceptance' do - allow(subject.project).to receive(:default_branch). - and_return(subject.target_branch) + allow(subject.project).to receive(:default_branch) + .and_return(subject.target_branch) closed = subject.closes_issues @@ -388,8 +388,8 @@ describe MergeRequest, models: true do subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}" allow(subject).to receive(:commits).and_return([commit]) - allow(subject.project).to receive(:default_branch). - and_return(subject.target_branch) + allow(subject.project).to receive(:default_branch) + .and_return(subject.target_branch) expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue]) end @@ -537,8 +537,8 @@ describe MergeRequest, models: true do subject.project.team << [subject.author, :developer] subject.description = "This issue Closes #{issue.to_reference}" - allow(subject.project).to receive(:default_branch). - and_return(subject.target_branch) + allow(subject.project).to receive(:default_branch) + .and_return(subject.target_branch) expect(subject.merge_commit_message) .to match("Closes #{issue.to_reference}") @@ -663,18 +663,18 @@ describe MergeRequest, models: true do end it 'caches the output' do - expect(subject).to receive(:compute_diverged_commits_count). - once. - and_return(2) + expect(subject).to receive(:compute_diverged_commits_count) + .once + .and_return(2) subject.diverged_commits_count subject.diverged_commits_count end it 'invalidates the cache when the source sha changes' do - expect(subject).to receive(:compute_diverged_commits_count). - twice. - and_return(2) + expect(subject).to receive(:compute_diverged_commits_count) + .twice + .and_return(2) subject.diverged_commits_count allow(subject).to receive(:source_branch_sha).and_return('123abc') @@ -682,9 +682,9 @@ describe MergeRequest, models: true do end it 'invalidates the cache when the target sha changes' do - expect(subject).to receive(:compute_diverged_commits_count). - twice. - and_return(2) + expect(subject).to receive(:compute_diverged_commits_count) + .twice + .and_return(2) subject.diverged_commits_count allow(subject).to receive(:target_branch_sha).and_return('123abc') @@ -706,8 +706,8 @@ describe MergeRequest, models: true do describe '#commits_sha' do before do - allow(subject.merge_request_diff).to receive(:commits_sha). - and_return(['sha1']) + allow(subject.merge_request_diff).to receive(:commits_sha) + .and_return(['sha1']) end it 'delegates to merge request diff' do @@ -892,7 +892,9 @@ describe MergeRequest, models: true do end context 'when broken' do - before { allow(subject).to receive(:broken?) { true } } + before do + allow(subject).to receive(:broken?) { true } + end it 'becomes unmergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') @@ -944,7 +946,9 @@ describe MergeRequest, models: true do end context 'when not open' do - before { subject.close } + before do + subject.close + end it 'returns false' do expect(subject.mergeable_state?).to be_falsey @@ -952,7 +956,9 @@ describe MergeRequest, models: true do end context 'when working in progress' do - before { subject.title = 'WIP MR' } + before do + subject.title = 'WIP MR' + end it 'returns false' do expect(subject.mergeable_state?).to be_falsey @@ -960,7 +966,9 @@ describe MergeRequest, models: true do end context 'when broken' do - before { allow(subject).to receive(:broken?) { true } } + before do + allow(subject).to receive(:broken?) { true } + end it 'returns false' do expect(subject.mergeable_state?).to be_falsey @@ -1389,7 +1397,7 @@ describe MergeRequest, models: true do end end - describe '#mergeable_with_slash_command?' do + describe '#mergeable_with_quick_action?' do def create_pipeline(status) pipeline = create(:ci_pipeline_with_one_job, project: project, @@ -1413,21 +1421,21 @@ describe MergeRequest, models: true do context 'when autocomplete_precheck is set to true' do it 'is mergeable by developer' do - expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy + expect(merge_request.mergeable_with_quick_action?(developer, autocomplete_precheck: true)).to be_truthy end it 'is not mergeable by normal user' do - expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey + expect(merge_request.mergeable_with_quick_action?(user, autocomplete_precheck: true)).to be_falsey end end context 'when autocomplete_precheck is set to false' do it 'is mergeable by developer' do - expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy end it 'is not mergeable by normal user' do - expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey + expect(merge_request.mergeable_with_quick_action?(user, last_diff_sha: mr_sha)).to be_falsey end context 'closed MR' do @@ -1436,7 +1444,7 @@ describe MergeRequest, models: true do end it 'is not mergeable' do - expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey end end @@ -1446,19 +1454,19 @@ describe MergeRequest, models: true do end it 'is not mergeable' do - expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey end end context 'sha differs from the MR diff_head_sha' do it 'is not mergeable' do - expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey + expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: 'some other sha')).to be_falsey end end context 'sha is not provided' do it 'is not mergeable' do - expect(merge_request.mergeable_with_slash_command?(developer)).to be_falsey + expect(merge_request.mergeable_with_quick_action?(developer)).to be_falsey end end @@ -1468,7 +1476,7 @@ describe MergeRequest, models: true do end it 'is mergeable' do - expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy end end @@ -1478,7 +1486,7 @@ describe MergeRequest, models: true do end it 'is not mergeable' do - expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey end end @@ -1488,7 +1496,7 @@ describe MergeRequest, models: true do end it 'is mergeable' do - expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy end end end @@ -1496,8 +1504,8 @@ describe MergeRequest, models: true do describe '#has_commits?' do before do - allow(subject.merge_request_diff).to receive(:commits_count). - and_return(2) + allow(subject.merge_request_diff).to receive(:commits_count) + .and_return(2) end it 'returns true when merge request diff has commits' do @@ -1507,8 +1515,8 @@ describe MergeRequest, models: true do describe '#has_no_commits?' do before do - allow(subject.merge_request_diff).to receive(:commits_count). - and_return(0) + allow(subject.merge_request_diff).to receive(:commits_count) + .and_return(0) end it 'returns true when merge request diff has 0 commits' do @@ -1566,4 +1574,40 @@ describe MergeRequest, models: true do end end end + + describe '#fetch_ref' do + it 'sets "ref_fetched" flag to true' do + subject.update!(ref_fetched: nil) + + subject.fetch_ref + + expect(subject.reload.ref_fetched).to be_truthy + end + end + + describe '#ref_fetched?' do + it 'does not perform git operation when value is cached' do + subject.ref_fetched = true + + expect_any_instance_of(Repository).not_to receive(:ref_exists?) + expect(subject.ref_fetched?).to be_truthy + end + + it 'caches the value when ref exists but value is not cached' do + subject.update!(ref_fetched: nil) + allow_any_instance_of(Repository).to receive(:ref_exists?) + .and_return(true) + + expect(subject.ref_fetched?).to be_truthy + expect(subject.reload.ref_fetched).to be_truthy + end + + it 'returns false when ref does not exist' do + subject.update!(ref_fetched: nil) + allow_any_instance_of(Repository).to receive(:ref_exists?) + .and_return(false) + + expect(subject.ref_fetched?).to be_falsey + end + end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index aa1ce89ffd7..45953023a36 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -144,35 +144,6 @@ describe Milestone, models: true do end end - describe '#sort_issues' do - let(:milestone) { create(:milestone) } - - let(:issue1) { create(:issue, milestone: milestone, position: 1) } - let(:issue2) { create(:issue, milestone: milestone, position: 2) } - let(:issue3) { create(:issue, milestone: milestone, position: 3) } - let(:issue4) { create(:issue, position: 42) } - - it 'sorts the given issues' do - milestone.sort_issues([issue3.id, issue2.id, issue1.id]) - - issue1.reload - issue2.reload - issue3.reload - - expect(issue1.position).to eq(3) - expect(issue2.position).to eq(2) - expect(issue3.position).to eq(1) - end - - it 'ignores issues not part of the milestone' do - milestone.sort_issues([issue3.id, issue2.id, issue1.id, issue4.id]) - - issue4.reload - - expect(issue4.position).to eq(42) - end - end - describe '.search' do let(:milestone) { create(:milestone, title: 'foo', description: 'bar') } @@ -193,13 +164,13 @@ describe Milestone, models: true do end it 'returns milestones with a partially matching description' do - expect(described_class.search(milestone.description[0..2])). - to eq([milestone]) + expect(described_class.search(milestone.description[0..2])) + .to eq([milestone]) end it 'returns milestones with a matching description regardless of the casing' do - expect(described_class.search(milestone.description.upcase)). - to eq([milestone]) + expect(described_class.search(milestone.description.upcase)) + .to eq([milestone]) end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 145c7ad5770..e7c3acf19eb 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -325,8 +325,8 @@ describe Namespace, models: true do describe '#user_ids_for_project_authorizations' do it 'returns the user IDs for which to refresh authorizations' do - expect(namespace.user_ids_for_project_authorizations). - to eq([namespace.owner_id]) + expect(namespace.user_ids_for_project_authorizations) + .to eq([namespace.owner_id]) end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 7a01cef9b4b..e2b80cb6e61 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -26,14 +26,18 @@ describe Note, models: true do it { is_expected.to validate_presence_of(:project) } context 'when note is on commit' do - before { allow(subject).to receive(:for_commit?).and_return(true) } + before do + allow(subject).to receive(:for_commit?).and_return(true) + end it { is_expected.to validate_presence_of(:commit_id) } it { is_expected.not_to validate_presence_of(:noteable_id) } end context 'when note is not on commit' do - before { allow(subject).to receive(:for_commit?).and_return(false) } + before do + allow(subject).to receive(:for_commit?).and_return(false) + end it { is_expected.not_to validate_presence_of(:commit_id) } it { is_expected.to validate_presence_of(:noteable_id) } @@ -148,8 +152,8 @@ describe Note, models: true do let!(:note2) { create(:note_on_issue) } it "reads the rendered note body from the cache" do - expect(Banzai::Renderer).to receive(:cache_collection_render). - with([{ + expect(Banzai::Renderer).to receive(:cache_collection_render) + .with([{ text: note1.note, context: { skip_project_check: false, @@ -160,8 +164,8 @@ describe Note, models: true do } }]).and_call_original - expect(Banzai::Renderer).to receive(:cache_collection_render). - with([{ + expect(Banzai::Renderer).to receive(:cache_collection_render) + .with([{ text: note2.note, context: { skip_project_check: false, @@ -402,8 +406,8 @@ describe Note, models: true do let(:note) { build(:note_on_project_snippet) } before do - expect(Banzai::Renderer).to receive(:cacheless_render_field). - with(note, :note, { skip_project_check: false }).and_return(html) + expect(Banzai::Renderer).to receive(:cacheless_render_field) + .with(note, :note, { skip_project_check: false }).and_return(html) note.save end @@ -417,8 +421,8 @@ describe Note, models: true do let(:note) { build(:note_on_personal_snippet) } before do - expect(Banzai::Renderer).to receive(:cacheless_render_field). - with(note, :note, { skip_project_check: true }).and_return(html) + expect(Banzai::Renderer).to receive(:cacheless_render_field) + .with(note, :note, { skip_project_check: true }).and_return(html) note.save end diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index d58673413c8..cc235ad467e 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -55,4 +55,34 @@ RSpec.describe NotificationSetting, type: :model do expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false)) end end + + describe 'event_enabled?' do + before do + subject.update!(user: create(:user)) + end + + context 'for an event with a matching column name' do + before do + subject.update!(events: { new_note: true }.to_json) + end + + it 'returns the value of the column' do + subject.update!(new_note: false) + + expect(subject.event_enabled?(:new_note)).to be(false) + end + + context 'when the column has a nil value' do + it 'returns the value from the events hash' do + expect(subject.event_enabled?(:new_note)).to be(false) + end + end + end + + context 'for an event without a matching column name' do + it 'returns false' do + expect(subject.event_enabled?(:foo_event)).to be(false) + end + end + end end diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb index cd0a4a94809..ee6bdc39c8c 100644 --- a/spec/models/project_authorization_spec.rb +++ b/spec/models/project_authorization_spec.rb @@ -7,8 +7,8 @@ describe ProjectAuthorization do describe '.insert_authorizations' do it 'inserts the authorizations' do - described_class. - insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]]) + described_class + .insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]]) expect(user.project_authorizations.count).to eq(1) end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 09a4448d387..580c83c12c0 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -4,6 +4,18 @@ describe ProjectFeature do let(:project) { create(:empty_project) } let(:user) { create(:user) } + describe '.quoted_access_level_column' do + it 'returns the table name and quoted column name for a feature' do + expected = if Gitlab::Database.postgresql? + '"project_features"."issues_access_level"' + else + '`project_features`.`issues_access_level`' + end + + expect(described_class.quoted_access_level_column(:issues)).to eq(expected) + end + end + describe '#feature_available?' do let(:features) { %w(issues wiki builds merge_requests snippets repository) } diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index 4014d6129ee..7b1a554d1fb 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -24,7 +24,9 @@ describe BambooService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:build_key) } it { is_expected.to validate_presence_of(:bamboo_url) } @@ -60,7 +62,9 @@ describe BambooService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:build_key) } it { is_expected.not_to validate_presence_of(:bamboo_url) } @@ -213,13 +217,13 @@ describe BambooService, models: true, caching: true do end def stub_request(status: 200, body: nil) - bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic' + bamboo_full_url = 'http://gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic' WebMock.stub_request(:get, bamboo_full_url).to_return( status: status, headers: { 'Content-Type' => 'application/json' }, body: body - ) + ).with(basic_auth: %w(mic password)) end def bamboo_response(result_key: 42, build_state: 'success', size: 1) diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb index 739cc72b2ff..5f17bbde390 100644 --- a/spec/models/project_services/bugzilla_service_spec.rb +++ b/spec/models/project_services/bugzilla_service_spec.rb @@ -8,7 +8,9 @@ describe BugzillaService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:issues_url) } @@ -19,7 +21,9 @@ describe BugzillaService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:issues_url) } diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 05b602d8106..dd529597067 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -23,7 +23,9 @@ describe BuildkiteService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:token) } @@ -31,7 +33,9 @@ describe BuildkiteService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:token) } diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb index 953e664fb66..56ff3596190 100644 --- a/spec/models/project_services/campfire_service_spec.rb +++ b/spec/models/project_services/campfire_service_spec.rb @@ -8,13 +8,17 @@ describe CampfireService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end @@ -35,21 +39,22 @@ describe CampfireService, models: true do room: 'test-room' ) @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) - @rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json' + @rooms_url = 'https://project-name.campfirenow.com/rooms.json' + @auth = %w(verySecret X) @headers = { 'Content-Type' => 'application/json; charset=utf-8' } end it "calls Campfire API to get a list of rooms and speak in a room" do # make sure a valid list of rooms is returned body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json') - WebMock.stub_request(:get, @rooms_url).to_return( + WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return( body: body, status: 200, headers: @headers ) # stub the speak request with the room id found in the previous request's response - speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/123/speak.json' - WebMock.stub_request(:post, speak_url) + speak_url = 'https://project-name.campfirenow.com/room/123/speak.json' + WebMock.stub_request(:post, speak_url).with(basic_auth: @auth) @campfire_service.execute(@sample_data) @@ -62,7 +67,7 @@ describe CampfireService, models: true do it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do # return a list of rooms that do not contain a room named 'test-room' body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json') - WebMock.stub_request(:get, @rooms_url).to_return( + WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return( body: body, status: 200, headers: @headers diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index 7d2599dc703..43b02568cb9 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -62,7 +62,7 @@ describe ChatMessage::PipelineMessage do def build_message(status_text = status, name = user[:name]) "<http://example.gitlab.com|project_name>:" \ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \ - " of branch `<http://example.gitlab.com/commits/develop|develop>`" \ + " of branch <http://example.gitlab.com/commits/develop|develop>" \ " by #{name} #{status_text} in 02:00:10" end end @@ -81,7 +81,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker passed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker passed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -98,7 +98,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker failed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker failed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -113,7 +113,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by API failed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by API failed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -125,7 +125,7 @@ describe ChatMessage::PipelineMessage do def build_markdown_message(status_text = status, name = user[:name]) "[project_name](http://example.gitlab.com):" \ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \ - " of branch `[develop](http://example.gitlab.com/commits/develop)`" \ + " of branch [develop](http://example.gitlab.com/commits/develop)" \ " by #{name} #{status_text} in 02:00:10" end end diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb index e38117b75f6..c794f659c41 100644 --- a/spec/models/project_services/chat_message/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -28,7 +28,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch `<http://url.com/commits/master|master>` of '\ + 'test.user pushed to branch <http://url.com/commits/master|master> of '\ '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)') expect(subject.attachments).to eq([{ text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\ @@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch `[master](http://url.com/commits/master)` of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))') + 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))') expect(subject.attachments).to eq( "[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2") expect(subject.activity).to eq({ @@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding pushes' do expect(subject.pretext).to eq('test.user pushed new tag ' \ - '`<http://url.com/commits/new_tag|new_tag>` to ' \ + '<http://url.com/commits/new_tag|new_tag> to ' \ '<http://url.com|project_name>') expect(subject.attachments).to be_empty end @@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed new tag `[new_tag](http://url.com/commits/new_tag)` to [project_name](http://url.com)') + 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user created tag', @@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch `<http://url.com/commits/master|master>` to '\ + 'test.user pushed new branch <http://url.com/commits/master|master> to '\ '<http://url.com|project_name>') expect(subject.attachments).to be_empty end @@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch `[master](http://url.com/commits/master)` to [project_name](http://url.com)') + 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user created branch', @@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch `master` from <http://url.com|project_name>') + 'test.user removed branch master from <http://url.com|project_name>') expect(subject.attachments).to be_empty end end @@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch `master` from [project_name](http://url.com)') + 'test.user removed branch master from [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user removed branch', diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb index 4ca1b8aa7b7..17355c1e6f1 100644 --- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -23,7 +23,9 @@ describe ChatMessage::WikiPageMessage, models: true do context 'without markdown' do describe '#pretext' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( @@ -33,7 +35,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( @@ -47,7 +51,9 @@ describe ChatMessage::WikiPageMessage, models: true do let(:color) { '#345' } context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns the attachment for a new wiki page' do expect(subject.attachments).to eq([ @@ -60,7 +66,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns the attachment for an updated wiki page' do expect(subject.attachments).to eq([ @@ -81,7 +89,9 @@ describe ChatMessage::WikiPageMessage, models: true do describe '#pretext' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( @@ -90,7 +100,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( @@ -101,7 +113,9 @@ describe ChatMessage::WikiPageMessage, models: true do describe '#attachments' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns the attachment for a new wiki page' do expect(subject.attachments).to eq('Wiki page description') @@ -109,7 +123,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns the attachment for an updated wiki page' do expect(subject.attachments).to eq('Wiki page description') @@ -119,7 +135,9 @@ describe ChatMessage::WikiPageMessage, models: true do describe '#activity' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns the attachment for a new wiki page' do expect(subject.activity).to eq({ @@ -132,7 +150,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns the attachment for an updated wiki page' do expect(subject.activity).to eq({ diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb index 63320931e76..9e574762232 100644 --- a/spec/models/project_services/custom_issue_tracker_service_spec.rb +++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb @@ -8,7 +8,9 @@ describe CustomIssueTrackerService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:issues_url) } @@ -19,7 +21,9 @@ describe CustomIssueTrackerService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:issues_url) } diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index 044737c6026..1400175427f 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -10,7 +10,9 @@ describe DroneCiService, models: true, caching: true do describe 'validations' do context 'active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } it { is_expected.to validate_presence_of(:drone_url) } @@ -18,7 +20,9 @@ describe DroneCiService, models: true, caching: true do end context 'inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } it { is_expected.not_to validate_presence_of(:drone_url) } diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb index e6f78898c82..d9b7010e5e5 100644 --- a/spec/models/project_services/emails_on_push_service_spec.rb +++ b/spec/models/project_services/emails_on_push_service_spec.rb @@ -3,13 +3,17 @@ require 'spec_helper' describe EmailsOnPushService do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:recipients) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:recipients) } end diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb index bdeea1db1e3..291fc645a1c 100644 --- a/spec/models/project_services/external_wiki_service_spec.rb +++ b/spec/models/project_services/external_wiki_service_spec.rb @@ -9,14 +9,18 @@ describe ExternalWikiService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:external_wiki_url) } it_behaves_like 'issue tracker service URL attribute', :external_wiki_url end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:external_wiki_url) } end diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index a97e8c6e4ce..56ace04dd58 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -8,13 +8,17 @@ describe FlowdockService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index a13fbae03eb..65c9e714bd1 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -8,14 +8,18 @@ describe GemnasiumService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } it { is_expected.to validate_presence_of(:api_key) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } it { is_expected.not_to validate_presence_of(:api_key) } diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 1200ae7eb22..c7c8e9651ab 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -8,13 +8,17 @@ describe HipchatService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb index d5a16226d9d..a5c4938b54e 100644 --- a/spec/models/project_services/irker_service_spec.rb +++ b/spec/models/project_services/irker_service_spec.rb @@ -10,13 +10,17 @@ describe IrkerService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:recipients) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:recipients) } end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 0ee050196e4..c86f56c55eb 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -10,7 +10,9 @@ describe JiraService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:url) } it { is_expected.to validate_presence_of(:project_key) } @@ -18,7 +20,9 @@ describe JiraService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:url) } end @@ -102,15 +106,15 @@ describe JiraService, models: true do @jira_service.save - project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' - @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' - @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' - @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink' + project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123' + @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions' + @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment' + @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink' - WebMock.stub_request(:get, project_issues_url) - WebMock.stub_request(:post, @transitions_url) - WebMock.stub_request(:post, @comment_url) - WebMock.stub_request(:post, @remote_link_url) + WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) end it "calls JIRA API" do @@ -198,9 +202,9 @@ describe JiraService, models: true do end def test_settings(api_url) - project_url = "http://jira_username:jira_password@#{api_url}/rest/api/2/project/GitLabProject" + project_url = "http://#{api_url}/rest/api/2/project/GitLabProject" - WebMock.stub_request(:get, project_url) + WebMock.stub_request(:get, project_url).with(basic_auth: %w(jira_username jira_password)) jira_service.test_settings end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 0dcf4a4b5d6..858ad595dbf 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -7,31 +7,15 @@ describe KubernetesService, models: true, caching: true do let(:project) { build_stubbed(:kubernetes_project) } let(:service) { project.kubernetes_service } - # We use Kubeclient to interactive with the Kubernetes API. It will - # GET /api/v1 for a list of resources the API supports. This must be stubbed - # in addition to any other HTTP requests we expect it to perform. - let(:discovery_url) { service.api_url + '/api/v1' } - let(:discovery_response) { { body: kube_discovery_body.to_json } } - - let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" } - let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } } - - def stub_kubeclient_discover - WebMock.stub_request(:get, discovery_url).to_return(discovery_response) - end - - def stub_kubeclient_pods - stub_kubeclient_discover - WebMock.stub_request(:get, pods_url).to_return(pods_response) - end - describe "Associations" do it { is_expected.to belong_to :project } end describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.not_to validate_presence_of(:namespace) } it { is_expected.to validate_presence_of(:api_url) } @@ -66,7 +50,9 @@ describe KubernetesService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:api_url) } it { is_expected.not_to validate_presence_of(:token) } @@ -87,7 +73,9 @@ describe KubernetesService, models: true, caching: true do end context 'as template' do - before { subject.template = true } + before do + subject.template = true + end it 'sets the namespace to the default' do expect(kube_namespace).not_to be_nil @@ -96,7 +84,9 @@ describe KubernetesService, models: true, caching: true do end context 'with associated project' do - before { subject.project = project } + before do + subject.project = project + end it 'sets the namespace to the default' do expect(kube_namespace).not_to be_nil @@ -111,6 +101,34 @@ describe KubernetesService, models: true, caching: true do it "returns the default namespace" do is_expected.to eq(service.send(:default_namespace)) end + + context 'when namespace is specified' do + before do + service.namespace = 'my-namespace' + end + + it "returns the user-namespace" do + is_expected.to eq('my-namespace') + end + end + + context 'when service is not assigned to project' do + before do + service.project = nil + end + + it "does not return namespace" do + is_expected.to be_nil + end + end + end + + describe '#actual_namespace' do + subject { service.actual_namespace } + + it "returns the default namespace" do + is_expected.to eq(service.send(:default_namespace)) + end context 'when namespace is specified' do before do @@ -134,6 +152,8 @@ describe KubernetesService, models: true, caching: true do end describe '#test' do + let(:discovery_url) { 'https://kubernetes.example.com/api/v1' } + before do stub_kubeclient_discover end @@ -142,7 +162,8 @@ describe KubernetesService, models: true, caching: true do let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } it 'tests with the prefix' do - service.api_url = 'https://kubernetes.example.com/prefix/' + service.api_url = 'https://kubernetes.example.com/prefix' + stub_kubeclient_discover expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once @@ -170,9 +191,9 @@ describe KubernetesService, models: true, caching: true do end context 'failure' do - let(:discovery_response) { { status: 404 } } - it 'fails to read the discovery endpoint' do + WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(status: 404) + expect(service.test[:success]).to be_falsy expect(WebMock).to have_requested(:get, discovery_url).once end @@ -188,7 +209,9 @@ describe KubernetesService, models: true, caching: true do end context 'namespace is provided' do - before { subject.namespace = 'my-project' } + before do + subject.namespace = 'my-project' + end it 'sets the variables' do expect(subject.predefined_variables).to include( @@ -258,27 +281,36 @@ describe KubernetesService, models: true, caching: true do end describe '#calculate_reactive_cache' do - before { stub_kubeclient_pods } subject { service.calculate_reactive_cache } context 'when service is inactive' do - before { service.active = false } + before do + service.active = false + end it { is_expected.to be_nil } end context 'when kubernetes responds with valid pods' do + before do + stub_kubeclient_pods + end + it { is_expected.to eq(pods: [kube_pod]) } end - context 'when kubernetes responds with 500' do - let(:pods_response) { { status: 500 } } + context 'when kubernetes responds with 500s' do + before do + stub_kubeclient_pods(status: 500) + end it { expect { subject }.to raise_error(KubeException) } end - context 'when kubernetes responds with 404' do - let(:pods_response) { { status: 404 } } + context 'when kubernetes responds with 404s' do + before do + stub_kubeclient_pods(status: 404) + end it { is_expected.to eq(pods: []) } end 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 f9531be5d25..fa38d23e82f 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -11,8 +11,8 @@ describe MattermostSlashCommandsService, :models do before do Mattermost::Session.base_uri("http://mattermost.example.com") - allow_any_instance_of(Mattermost::Client).to receive(:with_session). - and_yield(Mattermost::Session.new(nil)) + allow_any_instance_of(Mattermost::Client).to receive(:with_session) + .and_yield(Mattermost::Session.new(nil)) end describe '#configure' do @@ -24,8 +24,8 @@ describe MattermostSlashCommandsService, :models do context 'the requests succeeds' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - with(body: { + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + .with(body: { team_id: 'abc', trigger: 'gitlab', url: 'http://trigger.url', @@ -37,8 +37,8 @@ describe MattermostSlashCommandsService, :models do display_name: "GitLab / #{project.name_with_namespace}", method: 'P', username: 'GitLab' - }.to_json). - to_return( + }.to_json) + .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, body: { token: 'token' }.to_json @@ -58,8 +58,8 @@ describe MattermostSlashCommandsService, :models do context 'an error is received' do before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - to_return( + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create') + .to_return( status: 500, headers: { 'Content-Type' => 'application/json' }, body: { @@ -88,8 +88,8 @@ describe MattermostSlashCommandsService, :models do context 'the requests succeeds' do before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, body: { 'list' => true }.to_json @@ -103,8 +103,8 @@ describe MattermostSlashCommandsService, :models do context 'an error is received' do before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all') + .to_return( status: 500, headers: { 'Content-Type' => 'application/json' }, body: { diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index facc034f69c..bd50a2d1470 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -11,14 +11,18 @@ describe MicrosoftTeamsService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:webhook) } it_behaves_like 'issue tracker service URL attribute', :webhook end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:webhook) } end diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb index a76e909d04d..f4c1a9c94b6 100644 --- a/spec/models/project_services/pivotaltracker_service_spec.rb +++ b/spec/models/project_services/pivotaltracker_service_spec.rb @@ -8,13 +8,17 @@ describe PivotaltrackerService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index 1f9d3c07b51..37f23b1243c 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -14,13 +14,17 @@ describe PrometheusService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:api_url) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:api_url) } end @@ -61,13 +65,13 @@ describe PrometheusService, models: true, caching: true do end it 'returns reactive data' do - is_expected.to eq(prometheus_data) + is_expected.to eq(prometheus_metrics_data) end end end describe '#deployment_metrics' do - let(:deployment) { build_stubbed(:deployment)} + let(:deployment) { build_stubbed(:deployment) } let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery } around do |example| @@ -76,13 +80,16 @@ describe PrometheusService, models: true, caching: true do context 'with valid data' do subject { service.deployment_metrics(deployment) } + let(:fake_deployment_time) { 10 } before do stub_reactive_cache(service, prometheus_data, deployment_query, deployment.id) end it 'returns reactive data' do - is_expected.to eq(prometheus_data.merge(deployment_time: deployment.created_at.to_i)) + expect(deployment).to receive(:created_at).and_return(fake_deployment_time) + + expect(subject).to eq(prometheus_metrics_data.merge(deployment_time: fake_deployment_time)) end end end @@ -112,6 +119,7 @@ describe PrometheusService, models: true, caching: true do end it { expect(subject.to_json).to eq(prometheus_data.to_json) } + it { expect(subject.to_json).to eq(prometheus_data.to_json) } end [404, 500].each do |status| diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index a7e7594a7d5..9171d9604ee 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -8,7 +8,9 @@ describe PushoverService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:api_key) } it { is_expected.to validate_presence_of(:user_key) } @@ -16,7 +18,9 @@ describe PushoverService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:api_key) } it { is_expected.not_to validate_presence_of(:user_key) } diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb index 0a7b237a051..6631d9040b1 100644 --- a/spec/models/project_services/redmine_service_spec.rb +++ b/spec/models/project_services/redmine_service_spec.rb @@ -8,7 +8,9 @@ describe RedmineService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:issues_url) } @@ -19,7 +21,9 @@ describe RedmineService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:issues_url) } diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index 77b18e1c7d0..6b004098510 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -24,7 +24,9 @@ describe TeamcityService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:build_type) } it { is_expected.to validate_presence_of(:teamcity_url) } @@ -60,7 +62,9 @@ describe TeamcityService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:build_type) } it { is_expected.not_to validate_presence_of(:teamcity_url) } @@ -201,10 +205,12 @@ describe TeamcityService, models: true, caching: true do end def stub_request(status: 200, body: nil, build_status: 'success') - teamcity_full_url = 'http://mic:password@gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123' + teamcity_full_url = 'http://gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123' + auth = %w(mic password) + body ||= %Q({"build":{"status":"#{build_status}","id":"666"}}) - WebMock.stub_request(:get, teamcity_full_url).to_return( + WebMock.stub_request(:get, teamcity_full_url).with(basic_auth: auth).to_return( status: status, headers: { 'Content-Type' => 'application/json' }, body: body diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 454eeb58ecd..d7fcadb895e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1005,13 +1005,17 @@ describe Project, models: true do subject { project.shared_runners_enabled } context 'are enabled' do - before { stub_application_setting(shared_runners_enabled: true) } + before do + stub_application_setting(shared_runners_enabled: true) + end it { is_expected.to be_truthy } end context 'are disabled' do - before { stub_application_setting(shared_runners_enabled: false) } + before do + stub_application_setting(shared_runners_enabled: false) + end it { is_expected.to be_falsey } end @@ -1107,7 +1111,9 @@ describe Project, models: true do subject { project.pages_deployed? } context 'if public folder does exist' do - before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) } + before do + allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) + end it { is_expected.to be_truthy } end @@ -1189,23 +1195,23 @@ describe Project, models: true do it 'renames a repository' do stub_container_registry_config(enabled: false) - expect(gitlab_shell).to receive(:mv_repository). - ordered. - with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}"). - and_return(true) + expect(gitlab_shell).to receive(:mv_repository) + .ordered + .with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}") + .and_return(true) - expect(gitlab_shell).to receive(:mv_repository). - ordered. - with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki"). - and_return(true) + expect(gitlab_shell).to receive(:mv_repository) + .ordered + .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki") + .and_return(true) - expect_any_instance_of(SystemHooksService). - to receive(:execute_hooks_for). - with(project, :rename) + expect_any_instance_of(SystemHooksService) + .to receive(:execute_hooks_for) + .with(project, :rename) - expect_any_instance_of(Gitlab::UploadsTransfer). - to receive(:rename_project). - with('foo', project.path, project.namespace.full_path) + expect_any_instance_of(Gitlab::UploadsTransfer) + .to receive(:rename_project) + .with('foo', project.path, project.namespace.full_path) expect(project).to receive(:expire_caches_before_rename) @@ -1233,13 +1239,13 @@ describe Project, models: true do let(:wiki) { double(:wiki, exists?: true) } it 'expires the caches of the repository and wiki' do - allow(Repository).to receive(:new). - with('foo', project). - and_return(repo) + allow(Repository).to receive(:new) + .with('foo', project) + .and_return(repo) - allow(Repository).to receive(:new). - with('foo.wiki', project). - and_return(wiki) + allow(Repository).to receive(:new) + .with('foo.wiki', project) + .and_return(wiki) expect(repo).to receive(:before_delete) expect(wiki).to receive(:before_delete) @@ -1290,9 +1296,9 @@ describe Project, models: true do context 'using a regular repository' do it 'creates the repository' do - expect(shell).to receive(:add_repository). - with(project.repository_storage_path, project.path_with_namespace). - and_return(true) + expect(shell).to receive(:add_repository) + .with(project.repository_storage_path, project.path_with_namespace) + .and_return(true) expect(project.repository).to receive(:after_create) @@ -1300,9 +1306,9 @@ describe Project, models: true do end it 'adds an error if the repository could not be created' do - expect(shell).to receive(:add_repository). - with(project.repository_storage_path, project.path_with_namespace). - and_return(false) + expect(shell).to receive(:add_repository) + .with(project.repository_storage_path, project.path_with_namespace) + .and_return(false) expect(project.repository).not_to receive(:after_create) @@ -1365,7 +1371,9 @@ describe Project, models: true do subject { project.container_registry_url } - before { stub_container_registry_config(**registry_settings) } + before do + stub_container_registry_config(**registry_settings) + end context 'for enabled registry' do let(:registry_settings) do @@ -1389,7 +1397,9 @@ describe Project, models: true do let(:project) { create(:empty_project) } context 'when container registry is enabled' do - before { stub_container_registry_config(enabled: true) } + before do + stub_container_registry_config(enabled: true) + end context 'when tags are present for multi-level registries' do before do @@ -1427,7 +1437,9 @@ describe Project, models: true do end context 'when container registry is disabled' do - before { stub_container_registry_config(enabled: false) } + before do + stub_container_registry_config(enabled: false) + end it 'should not have image tags' do expect(project).not_to have_container_registry_tags @@ -1552,8 +1564,8 @@ describe Project, models: true do let(:project) { forked_project_link.forked_to_project } it 'schedules a RepositoryForkWorker job' do - expect(RepositoryForkWorker).to receive(:perform_async). - with(project.id, forked_from_project.repository_storage_path, + expect(RepositoryForkWorker).to receive(:perform_async) + .with(project.id, forked_from_project.repository_storage_path, forked_from_project.path_with_namespace, project.namespace.full_path) project.add_import_job @@ -1945,7 +1957,9 @@ describe Project, models: true do describe '#parent_changed?' do let(:project) { create(:empty_project) } - before { project.namespace_id = 7 } + before do + project.namespace_id = 7 + end it { expect(project.parent_changed?).to be_truthy } end @@ -2027,15 +2041,15 @@ describe Project, models: true do error_message = 'Failed to replace merge_requests because one or more of the new records could not be saved.'\ ' Validate fork Source project is not a fork of the target project' - expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) }. - to raise_error(ActiveRecord::RecordNotSaved, error_message) + expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) } + .to raise_error(ActiveRecord::RecordNotSaved, error_message) end it 'updates the project succesfully' do merge_request = create(:merge_request, target_project: project, source_project: project) - expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }. - not_to raise_error + expect { project.append_or_update_attribute(:merge_requests, [merge_request]) } + .not_to raise_error end end @@ -2046,4 +2060,36 @@ describe Project, models: true do expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i) end end + + describe '.public_or_visible_to_user' do + let!(:user) { create(:user) } + + let!(:private_project) do + create(:empty_project, :private, creator: user, namespace: user.namespace) + end + + let!(:public_project) { create(:empty_project, :public) } + + context 'with a user' do + let(:projects) do + Project.all.public_or_visible_to_user(user) + end + + it 'includes projects the user has access to' do + expect(projects).to include(private_project) + end + + it 'includes projects the user can see' do + expect(projects).to include(public_project) + end + end + + context 'without a user' do + it 'only includes public projects' do + projects = Project.all.public_or_visible_to_user + + expect(projects).to eq([public_project]) + end + end + end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 497e3cdf415..49f2f8c0ad1 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -100,8 +100,8 @@ describe ProjectTeam, models: true do group_access: Gitlab::Access::GUEST ) - expect(project.team.members). - to contain_exactly(group_member.user, project.owner) + expect(project.team.members) + .to contain_exactly(group_member.user, project.owner) end it 'returns invited members of a group of a specified level' do @@ -240,7 +240,9 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } context 'but share_with_group_lock is true' do - before { project.namespace.update(share_with_group_lock: true) } + before do + project.namespace.update(share_with_group_lock: true) + end it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) } diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 224067f58dd..bf74ac5ea25 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -8,7 +8,10 @@ describe ProjectWiki, models: true do let(:project_wiki) { ProjectWiki.new(project, user) } subject { project_wiki } - before { project_wiki.wiki } + + before do + project_wiki.wiki + end describe "#path_with_namespace" do it "returns the project path with namespace with the .wiki extension" do @@ -146,15 +149,15 @@ describe ProjectWiki, models: true do describe '#find_file' do before do file = Gollum::File.new(subject.wiki) - allow_any_instance_of(Gollum::Wiki). - to receive(:file).with('image.jpg', 'master', true). - and_return(file) - allow_any_instance_of(Gollum::File). - to receive(:mime_type). - and_return('image/jpeg') - allow_any_instance_of(Gollum::Wiki). - to receive(:file).with('non-existant', 'master', true). - and_return(nil) + allow_any_instance_of(Gollum::Wiki) + .to receive(:file).with('image.jpg', 'master', true) + .and_return(file) + allow_any_instance_of(Gollum::File) + .to receive(:mime_type) + .and_return('image/jpeg') + allow_any_instance_of(Gollum::Wiki) + .to receive(:file).with('non-existant', 'master', true) + .and_return(nil) end after do @@ -265,9 +268,9 @@ describe ProjectWiki, models: true do describe '#create_repo!' do it 'creates a repository' do - expect(subject).to receive(:init_repo). - with(subject.path_with_namespace). - and_return(true) + expect(subject).to receive(:init_repo) + .with(subject.path_with_namespace) + .and_return(true) expect(subject.repository).to receive(:after_create) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a6d4d92c450..3e984ec7588 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -111,8 +111,8 @@ describe Repository, models: true do describe '#ref_name_for_sha' do it 'returns the ref' do - allow(repository.raw_repository).to receive(:ref_name_for_sha). - and_return('refs/environments/production/77') + allow(repository.raw_repository).to receive(:ref_name_for_sha) + .and_return('refs/environments/production/77') expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77' end @@ -593,8 +593,8 @@ describe Repository, models: true do user, 'LICENSE', 'Copyright!', message: 'Add LICENSE', branch_name: 'master') - allow(repository).to receive(:file_on_head). - and_raise(Rugged::ReferenceError) + allow(repository).to receive(:file_on_head) + .and_raise(Rugged::ReferenceError) expect(repository.license_blob).to be_nil end @@ -779,8 +779,8 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do - expect_any_instance_of(GitHooksService).to receive(:execute). - with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') + expect_any_instance_of(GitHooksService).to receive(:execute) + .with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -822,14 +822,14 @@ describe Repository, models: true do before do service = GitHooksService.new expect(GitHooksService).to receive(:new).and_return(service) - expect(service).to receive(:execute). - with( + expect(service).to receive(:execute) + .with( user, repository.path_to_repo, old_rev, new_rev, - 'refs/heads/feature'). - and_yield(service).and_return(true) + 'refs/heads/feature') + .and_yield(service).and_return(true) end it 'runs without errors' do @@ -923,8 +923,8 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) - GitOperationService.new(user, repository). - with_branch('new-feature') do + GitOperationService.new(user, repository) + .with_branch('new-feature') do new_rev end end @@ -1007,8 +1007,8 @@ describe Repository, models: true do end it 'does nothing' do - expect(repository.raw_repository).not_to receive(:autocrlf=). - with(:input) + expect(repository.raw_repository).not_to receive(:autocrlf=) + .with(:input) GitOperationService.new(nil, repository).send(:update_autocrlf_option) end @@ -1027,9 +1027,9 @@ describe Repository, models: true do end it 'caches the output' do - expect(repository.raw_repository).to receive(:empty?). - once. - and_return(false) + expect(repository.raw_repository).to receive(:empty?) + .once + .and_return(false) repository.empty? repository.empty? @@ -1042,9 +1042,9 @@ describe Repository, models: true do end it 'caches the output' do - expect(repository.raw_repository).to receive(:root_ref). - once. - and_return('master') + expect(repository.raw_repository).to receive(:root_ref) + .once + .and_return('master') repository.root_ref repository.root_ref @@ -1055,9 +1055,9 @@ describe Repository, models: true do it 'expires the root reference cache' do repository.root_ref - expect(repository.raw_repository).to receive(:root_ref). - once. - and_return('foo') + expect(repository.raw_repository).to receive(:root_ref) + .once + .and_return('foo') repository.expire_root_ref_cache @@ -1071,17 +1071,17 @@ describe Repository, models: true do let(:cache) { repository.send(:cache) } it 'expires the cache for all branches' do - expect(cache).to receive(:expire). - at_least(repository.branches.length * 2). - times + expect(cache).to receive(:expire) + .at_least(repository.branches.length * 2) + .times repository.expire_branch_cache end it 'expires the cache for all branches when the root branch is given' do - expect(cache).to receive(:expire). - at_least(repository.branches.length * 2). - times + expect(cache).to receive(:expire) + .at_least(repository.branches.length * 2) + .times repository.expire_branch_cache(repository.root_ref) end @@ -1344,12 +1344,12 @@ describe Repository, models: true do describe '#after_push_commit' do it 'expires statistics caches' do - expect(repository).to receive(:expire_statistics_caches). - and_call_original + expect(repository).to receive(:expire_statistics_caches) + .and_call_original - expect(repository).to receive(:expire_branch_cache). - with('master'). - and_call_original + expect(repository).to receive(:expire_branch_cache) + .with('master') + .and_call_original repository.after_push_commit('master') end @@ -1434,9 +1434,9 @@ describe Repository, models: true do describe '#expire_branches_cache' do it 'expires the cache' do - expect(repository).to receive(:expire_method_caches). - with(%i(branch_names branch_count)). - and_call_original + expect(repository).to receive(:expire_method_caches) + .with(%i(branch_names branch_count)) + .and_call_original repository.expire_branches_cache end @@ -1444,9 +1444,9 @@ describe Repository, models: true do describe '#expire_tags_cache' do it 'expires the cache' do - expect(repository).to receive(:expire_method_caches). - with(%i(tag_names tag_count)). - and_call_original + expect(repository).to receive(:expire_method_caches) + .with(%i(tag_names tag_count)) + .and_call_original repository.expire_tags_cache end @@ -1457,11 +1457,11 @@ describe Repository, models: true do let(:user) { build_stubbed(:user) } it 'creates the tag using rugged' do - expect(repository.rugged.tags).to receive(:create). - with('8.5', repository.commit('master').id, + expect(repository.rugged.tags).to receive(:create) + .with('8.5', repository.commit('master').id, hash_including(message: 'foo', - tagger: hash_including(name: user.name, email: user.email))). - and_call_original + tagger: hash_including(name: user.name, email: user.email))) + .and_call_original repository.add_tag(user, '8.5', 'master', 'foo') end @@ -1478,8 +1478,8 @@ describe Repository, models: true do update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo) post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo) - allow(Gitlab::Git::Hook).to receive(:new). - and_return(pre_receive_hook, update_hook, post_receive_hook) + allow(Gitlab::Git::Hook).to receive(:new) + .and_return(pre_receive_hook, update_hook, post_receive_hook) allow(pre_receive_hook).to receive(:trigger).and_call_original allow(update_hook).to receive(:trigger).and_call_original @@ -1490,12 +1490,12 @@ describe Repository, models: true do commit_sha = repository.commit('master').id tag_sha = tag.target - expect(pre_receive_hook).to have_received(:trigger). - with(anything, anything, commit_sha, anything) - expect(update_hook).to have_received(:trigger). - with(anything, anything, commit_sha, anything) - expect(post_receive_hook).to have_received(:trigger). - with(anything, anything, tag_sha, anything) + expect(pre_receive_hook).to have_received(:trigger) + .with(anything, anything, commit_sha, anything) + expect(update_hook).to have_received(:trigger) + .with(anything, anything, commit_sha, anything) + expect(post_receive_hook).to have_received(:trigger) + .with(anything, anything, tag_sha, anything) end end @@ -1529,25 +1529,25 @@ describe Repository, models: true do describe '#avatar' do it 'returns nil if repo does not exist' do - expect(repository).to receive(:file_on_head). - and_raise(Rugged::ReferenceError) + expect(repository).to receive(:file_on_head) + .and_raise(Rugged::ReferenceError) expect(repository.avatar).to eq(nil) end it 'returns the first avatar file found in the repository' do - expect(repository).to receive(:file_on_head). - with(:avatar). - and_return(double(:tree, path: 'logo.png')) + expect(repository).to receive(:file_on_head) + .with(:avatar) + .and_return(double(:tree, path: 'logo.png')) expect(repository.avatar).to eq('logo.png') end it 'caches the output' do - expect(repository).to receive(:file_on_head). - with(:avatar). - once. - and_return(double(:tree, path: 'logo.png')) + expect(repository).to receive(:file_on_head) + .with(:avatar) + .once + .and_return(double(:tree, path: 'logo.png')) 2.times { expect(repository.avatar).to eq('logo.png') } end @@ -1607,24 +1607,24 @@ describe Repository, models: true do describe '#contribution_guide', caching: true do it 'returns and caches the output' do - expect(repository).to receive(:file_on_head). - with(:contributing). - and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md')). - once + expect(repository).to receive(:file_on_head) + .with(:contributing) + .and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md')) + .once 2.times do - expect(repository.contribution_guide). - to be_an_instance_of(Gitlab::Git::Tree) + expect(repository.contribution_guide) + .to be_an_instance_of(Gitlab::Git::Tree) end end end describe '#gitignore', caching: true do it 'returns and caches the output' do - expect(repository).to receive(:file_on_head). - with(:gitignore). - and_return(Gitlab::Git::Tree.new(path: '.gitignore')). - once + expect(repository).to receive(:file_on_head) + .with(:gitignore) + .and_return(Gitlab::Git::Tree.new(path: '.gitignore')) + .once 2.times do expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree) @@ -1634,10 +1634,10 @@ describe Repository, models: true do describe '#koding_yml', caching: true do it 'returns and caches the output' do - expect(repository).to receive(:file_on_head). - with(:koding). - and_return(Gitlab::Git::Tree.new(path: '.koding.yml')). - once + expect(repository).to receive(:file_on_head) + .with(:koding) + .and_return(Gitlab::Git::Tree.new(path: '.koding.yml')) + .once 2.times do expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree) @@ -1673,8 +1673,8 @@ describe Repository, models: true do describe '#expire_statistics_caches' do it 'expires the caches' do - expect(repository).to receive(:expire_method_caches). - with(%i(size commit_count)) + expect(repository).to receive(:expire_method_caches) + .with(%i(size commit_count)) repository.expire_statistics_caches end @@ -1691,8 +1691,8 @@ describe Repository, models: true do describe '#expire_all_method_caches' do it 'expires the caches of all methods' do - expect(repository).to receive(:expire_method_caches). - with(Repository::CACHED_METHODS) + expect(repository).to receive(:expire_method_caches) + .with(Repository::CACHED_METHODS) repository.expire_all_method_caches end @@ -1717,8 +1717,8 @@ describe Repository, models: true do context 'with an existing repository' do it 'returns a Gitlab::Git::Tree' do - expect(repository.file_on_head(:readme)). - to be_an_instance_of(Gitlab::Git::Tree) + expect(repository.file_on_head(:readme)) + .to be_an_instance_of(Gitlab::Git::Tree) end end end @@ -1856,8 +1856,8 @@ describe Repository, models: true do describe '#refresh_method_caches' do it 'refreshes the caches of the given types' do - expect(repository).to receive(:expire_method_caches). - with(%i(rendered_readme license_blob license_key license)) + expect(repository).to receive(:expire_method_caches) + .with(%i(rendered_readme license_blob license_key license)) expect(repository).to receive(:rendered_readme) expect(repository).to receive(:license_blob) diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index c1fe1b06c52..1754253e0f2 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -9,7 +9,10 @@ describe Route, models: true do end describe 'validations' do - before { route } + before do + expect(route).to be_persisted + end + it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path) } @@ -59,7 +62,9 @@ describe Route, models: true do context 'path update' do context 'when route name is set' do - before { route.update_attributes(path: 'bar') } + before do + route.update_attributes(path: 'bar') + end it 'updates children routes with new path' do expect(described_class.exists?(path: 'bar')).to be_truthy diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 4c832c87d6a..2dea2c6015f 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -54,8 +54,8 @@ describe Upload, type: :model do uploader: 'AvatarUploader' ) - expect { described_class.remove_path(__FILE__) }. - to change { described_class.count }.from(1).to(0) + expect { described_class.remove_path(__FILE__) } + .to change { described_class.count }.from(1).to(0) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d5bd9946ab6..8e895ec6634 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -451,6 +451,40 @@ describe User, models: true do end end + describe '#ensure_user_rights_and_limits' do + describe 'with external user' do + let(:user) { create(:user, external: true) } + + it 'receives callback when external changes' do + expect(user).to receive(:ensure_user_rights_and_limits) + + user.update_attributes(external: false) + end + + it 'ensures correct rights and limits for user' do + stub_config_setting(default_can_create_group: true) + + expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true) + .and change { user.projects_limit }.to(current_application_settings.default_projects_limit) + end + end + + describe 'without external user' do + let(:user) { create(:user, external: false) } + + it 'receives callback when external changes' do + expect(user).to receive(:ensure_user_rights_and_limits) + + user.update_attributes(external: true) + end + + it 'ensures correct rights and limits for user' do + expect { user.update_attributes(external: true) }.to change { user.can_create_group }.to(false) + .and change { user.projects_limit }.to(0) + end + end + end + describe 'rss token' do it 'ensures an rss token on read' do user = create(:user, rss_token: nil) @@ -878,8 +912,8 @@ describe User, models: true do describe '.find_by_username!' do it 'raises RecordNotFound' do - expect { described_class.find_by_username!('JohnDoe') }. - to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.find_by_username!('JohnDoe') } + .to raise_error(ActiveRecord::RecordNotFound) end it 'is case-insensitive' do @@ -1523,8 +1557,8 @@ describe User, models: true do end it 'returns the projects when using an ActiveRecord relation' do - projects = user. - projects_with_reporter_access_limited_to(Project.select(:id)) + projects = user + .projects_with_reporter_access_limited_to(Project.select(:id)) expect(projects).to eq([project1]) end @@ -1584,7 +1618,9 @@ describe User, models: true do end context 'user is member of the top group' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end if Group.supports_nested_groups? it 'returns all groups' do @@ -1602,7 +1638,9 @@ describe User, models: true do end context 'user is member of the first child (internal node), branch 1', :nested_groups do - before { nested_group_1.add_owner(user) } + before do + nested_group_1.add_owner(user) + end it 'returns the groups in the hierarchy' do is_expected.to match_array [ @@ -1613,7 +1651,9 @@ describe User, models: true do end context 'user is member of the first child (internal node), branch 2', :nested_groups do - before { nested_group_2.add_owner(user) } + before do + nested_group_2.add_owner(user) + end it 'returns the groups in the hierarchy' do is_expected.to match_array [ @@ -1624,7 +1664,9 @@ describe User, models: true do end context 'user is member of the last child (leaf node)', :nested_groups do - before { nested_group_1_1.add_owner(user) } + before do + nested_group_1_1.add_owner(user) + end it 'returns the groups in the hierarchy' do is_expected.to match_array [ @@ -1691,6 +1733,20 @@ describe User, models: true do end end + describe '#full_private_access?' do + it 'returns false for regular user' do + user = build(:user) + + expect(user.full_private_access?).to be_falsy + end + + it 'returns true for admin user' do + user = build(:user, :admin) + + expect(user.full_private_access?).to be_truthy + end + end + describe '.ghost' do it "creates a ghost user if one isn't already present" do ghost = User.ghost diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 753dc938c52..4a73552b8a6 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -61,8 +61,8 @@ describe WikiPage, models: true do actual_order = grouped_entries.map do |page_or_dir| get_slugs(page_or_dir) - end. - flatten + end + .flatten expect(actual_order).to eq(expected_order) end end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 3f4ce222b60..48a139d4b83 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -10,7 +10,9 @@ describe Ci::BuildPolicy, :models do end shared_context 'public pipelines disabled' do - before { project.update_attribute(:public_builds, false) } + before do + project.update_attribute(:public_builds, false) + end end describe '#rules' do @@ -54,7 +56,9 @@ describe Ci::BuildPolicy, :models do let(:project) { create(:empty_project, :public) } context 'team member is a guest' do - before { project.team << [user, :guest] } + before do + project.team << [user, :guest] + end context 'when public builds are enabled' do it 'includes ability to read build' do @@ -72,7 +76,9 @@ describe Ci::BuildPolicy, :models do end context 'team member is a reporter' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end context 'when public builds are enabled' do it 'includes ability to read build' do diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 0d3af1f4499..d70e15f006b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -80,8 +80,8 @@ describe ProjectPolicy, models: true do expect(project.team.member?(issue.author)).to eq(false) - expect(BasePolicy.class_for(project).abilities(user, project).can_set). - not_to include(:read_issue) + expect(BasePolicy.class_for(project).abilities(user, project).can_set) + .not_to include(:read_issue) expect(Ability.allowed?(user, :read_issue, project)).to be_falsy end @@ -139,6 +139,18 @@ describe ProjectPolicy, models: true do is_expected.not_to include(:read_build, :read_pipeline) end end + + context 'when builds are disabled' do + before do + project.project_feature.update( + builds_access_level: ProjectFeature::DISABLED) + end + + it do + is_expected.not_to include(:read_build) + is_expected.to include(:read_pipeline) + end + end end context 'reporter' do diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index ddbed5f781e..d2b2528c57a 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -78,7 +78,9 @@ describe ProjectSnippetPolicy, models: true do context 'project team member external user' do subject { abilities(external_user, :internal) } - before { project.team << [external_user, :developer] } + before do + project.team << [external_user, :developer] + end it do is_expected.to include(:read_project_snippet) @@ -120,7 +122,9 @@ describe ProjectSnippetPolicy, models: true do context 'project team member normal user' do subject { abilities(regular_user, :private) } - before { project.team << [regular_user, :developer] } + before do + project.team << [regular_user, :developer] + end it do is_expected.to include(:read_project_snippet) @@ -131,7 +135,9 @@ describe ProjectSnippetPolicy, models: true do context 'project team member external user' do subject { abilities(external_user, :private) } - before { project.team << [external_user, :developer] } + before do + project.team << [external_user, :developer] + end it do is_expected.to include(:read_project_snippet) diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index 2190ab0e82e..518e97d17a1 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -47,8 +47,8 @@ describe Ci::BuildPresenter do context 'when build is erased' do before do expect(presenter).to receive(:erased_by_user?).and_return(true) - expect(build).to receive(:erased_by). - and_return(double(:user, name: 'John Doe')) + expect(build).to receive(:erased_by) + .and_return(double(:user, name: 'John Doe')) end it 'returns the name of the eraser' do diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index bbdef0aeb1b..6d822b5cb4f 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -9,7 +9,9 @@ describe API::AwardEmoji do let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } let!(:note) { create(:note, project: project, noteable: issue) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do context 'on an issue' do diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 6b637a03b6f..cdb60fc0d1a 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -34,7 +34,9 @@ describe API::CommitStatuses do let!(:status6) { create_status(master, status: 'success') } context 'latest commit statuses' do - before { get api(get_url, reporter) } + before do + get api(get_url, reporter) + end it 'returns latest commit statuses' do expect(response).to have_http_status(200) @@ -48,7 +50,9 @@ describe API::CommitStatuses do end context 'all commit statuses' do - before { get api(get_url, reporter), all: 1 } + before do + get api(get_url, reporter), all: 1 + end it 'returns all commit statuses' do expect(response).to have_http_status(200) @@ -61,7 +65,9 @@ describe API::CommitStatuses do end context 'latest commit statuses for specific ref' do - before { get api(get_url, reporter), ref: 'develop' } + before do + get api(get_url, reporter), ref: 'develop' + end it 'returns latest commit statuses for specific ref' do expect(response).to have_http_status(200) @@ -72,7 +78,9 @@ describe API::CommitStatuses do end context 'latest commit statues for specific name' do - before { get api(get_url, reporter), name: 'coverage' } + before do + get api(get_url, reporter), name: 'coverage' + end it 'return latest commit statuses for specific name' do expect(response).to have_http_status(200) @@ -85,7 +93,9 @@ describe API::CommitStatuses do end context 'ci commit does not exist' do - before { get api(get_url, reporter) } + before do + get api(get_url, reporter) + end it 'returns empty array' do expect(response.status).to eq 200 @@ -95,7 +105,9 @@ describe API::CommitStatuses do end context "guest user" do - before { get api(get_url, guest) } + before do + get api(get_url, guest) + end it "does not return project commits" do expect(response).to have_http_status(403) @@ -103,7 +115,9 @@ describe API::CommitStatuses do end context "unauthorized user" do - before { get api(get_url) } + before do + get api(get_url) + end it "does not return project commits" do expect(response).to have_http_status(401) @@ -150,25 +164,40 @@ describe API::CommitStatuses do context 'with all optional parameters' do context 'when creating a commit status' do - it 'creates commit status' do + subject do post api(post_url, developer), { state: 'success', context: 'coverage', - ref: 'develop', + ref: 'master', description: 'test', coverage: 80.0, target_url: 'http://gitlab.com/status' } + end + + it 'creates commit status' do + subject expect(response).to have_http_status(201) expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') expect(json_response['name']).to eq('coverage') - expect(json_response['ref']).to eq('develop') + expect(json_response['ref']).to eq('master') expect(json_response['coverage']).to eq(80.0) expect(json_response['description']).to eq('test') expect(json_response['target_url']).to eq('http://gitlab.com/status') end + + context 'when merge request exists for given branch' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'develop') } + + it 'sets head pipeline' do + subject + + expect(response).to have_http_status(201) + expect(merge_request.reload.head_pipeline).not_to be_nil + end + end end context 'when updatig a commit status' do @@ -176,7 +205,7 @@ describe API::CommitStatuses do post api(post_url, developer), { state: 'running', context: 'coverage', - ref: 'develop', + ref: 'master', description: 'coverage test', coverage: 0.0, target_url: 'http://gitlab.com/status' @@ -185,7 +214,7 @@ describe API::CommitStatuses do post api(post_url, developer), { state: 'success', name: 'coverage', - ref: 'develop', + ref: 'master', description: 'new description', coverage: 90.0 } @@ -196,7 +225,7 @@ describe API::CommitStatuses do expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') expect(json_response['name']).to eq('coverage') - expect(json_response['ref']).to eq('develop') + expect(json_response['ref']).to eq('master') expect(json_response['coverage']).to eq(90.0) expect(json_response['description']).to eq('new description') expect(json_response['target_url']).to eq('http://gitlab.com/status') @@ -209,7 +238,9 @@ describe API::CommitStatuses do end context 'when status is invalid' do - before { post api(post_url, developer), state: 'invalid' } + before do + post api(post_url, developer), state: 'invalid' + end it 'does not create commit status' do expect(response).to have_http_status(400) @@ -217,7 +248,9 @@ describe API::CommitStatuses do end context 'when request without a state made' do - before { post api(post_url, developer) } + before do + post api(post_url, developer) + end it 'does not create commit status' do expect(response).to have_http_status(400) @@ -226,7 +259,10 @@ describe API::CommitStatuses do context 'when commit SHA is invalid' do let(:sha) { 'invalid_sha' } - before { post api(post_url, developer), state: 'running' } + + before do + post api(post_url, developer), state: 'running' + end it 'returns not found error' do expect(response).to have_http_status(404) @@ -248,7 +284,9 @@ describe API::CommitStatuses do end context 'reporter user' do - before { post api(post_url, reporter), state: 'running' } + before do + post api(post_url, reporter), state: 'running' + end it 'does not create commit status' do expect(response).to have_http_status(403) @@ -256,7 +294,9 @@ describe API::CommitStatuses do end context 'guest user' do - before { post api(post_url, guest), state: 'running' } + before do + post api(post_url, guest), state: 'running' + end it 'does not create commit status' do expect(response).to have_http_status(403) @@ -264,7 +304,9 @@ describe API::CommitStatuses do end context 'unauthorized user' do - before { post api(post_url) } + before do + post api(post_url) + end it 'does not create commit status' do expect(response).to have_http_status(401) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index b0c265b6453..0dad547735d 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -9,11 +9,15 @@ describe API::Commits do let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end describe "List repository commits" do context "authorized user" do - before { project.team << [user2, :reporter] } + before do + project.team << [user2, :reporter] + end it "returns project commits" do commit = project.repository.commit @@ -514,7 +518,9 @@ describe API::Commits do describe "Get the diff of a commit" do context "authorized user" do - before { project.team << [user2, :reporter] } + before do + project.team << [user2, :reporter] + end it "returns the diff of the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 4d9cd5f3a27..32439981b60 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -41,7 +41,9 @@ describe API::DeployKeys do end describe 'GET /projects/:id/deploy_keys' do - before { deploy_key } + before do + deploy_key + end it 'returns array of ssh keys' do get api("/projects/#{project.id}/deploy_keys", admin) @@ -158,10 +160,22 @@ describe API::DeployKeys do expect(json_response['title']).to eq('new title') expect(json_response['can_push']).to eq(true) end + + it 'updates a private ssh key from projects user has access with correct attributes' do + create(:deploy_keys_project, project: project2, deploy_key: private_deploy_key) + + put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: 'new title', can_push: true } + + expect(json_response['id']).to eq(private_deploy_key.id) + expect(json_response['title']).to eq('new title') + expect(json_response['can_push']).to eq(true) + end end describe 'DELETE /projects/:id/deploy_keys/:key_id' do - before { deploy_key } + before do + deploy_key + end it 'deletes existing key' do expect do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index d325c6eff9d..9e268adf950 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -13,7 +13,9 @@ describe API::Files do let(:author_email) { 'user@example.org' } let(:author_name) { 'John Doe' } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end def route(file_path = nil) "/projects/#{project.id}/repository/files/#{file_path}" @@ -203,8 +205,8 @@ describe API::Files do end it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:create_file). - and_raise(Repository::CommitError, 'Cannot create file') + allow_any_instance_of(Repository).to receive(:create_file) + .and_raise(Repository::CommitError, 'Cannot create file') post api(route("any%2Etxt"), user), valid_params diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index bb53796cbd7..656f098aea8 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -513,8 +513,8 @@ describe API::Groups do let(:project_path) { project.full_path.gsub('/', '%2F') } before(:each) do - allow_any_instance_of(Projects::TransferService). - to receive(:execute).and_return(true) + allow_any_instance_of(Projects::TransferService) + .to receive(:execute).and_return(true) end context "when authenticated as user" do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index ed392acc607..191c60aba31 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -55,40 +55,62 @@ describe API::Helpers do subject { current_user } describe "Warden authentication" do - before { doorkeeper_guard_returns false } + before do + doorkeeper_guard_returns false + end context "with invalid credentials" do context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } + before do + env['REQUEST_METHOD'] = 'GET' + end + it { is_expected.to be_nil } end end context "with valid credentials" do - before { warden_authenticate_returns user } + before do + warden_authenticate_returns user + end context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } + before do + env['REQUEST_METHOD'] = 'GET' + end + it { is_expected.to eq(user) } end context "HEAD request" do - before { env['REQUEST_METHOD'] = 'HEAD' } + before do + env['REQUEST_METHOD'] = 'HEAD' + end + it { is_expected.to eq(user) } end context "PUT request" do - before { env['REQUEST_METHOD'] = 'PUT' } + before do + env['REQUEST_METHOD'] = 'PUT' + end + it { is_expected.to be_nil } end context "POST request" do - before { env['REQUEST_METHOD'] = 'POST' } + before do + env['REQUEST_METHOD'] = 'POST' + end + it { is_expected.to be_nil } end context "DELETE request" do - before { env['REQUEST_METHOD'] = 'DELETE' } + before do + env['REQUEST_METHOD'] = 'DELETE' + end + it { is_expected.to be_nil } end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index cf232e7ff69..6deaea956e0 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -15,21 +15,43 @@ describe API::Internal do end end - describe "GET /internal/broadcast_message" do - context "broadcast message exists" do - let!(:broadcast_message) { create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow ) } + describe 'GET /internal/broadcast_message' do + context 'broadcast message exists' do + let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } - it do - get api("/internal/broadcast_message"), secret_token: secret_token + it 'returns one broadcast message' do + get api('/internal/broadcast_message'), secret_token: secret_token expect(response).to have_http_status(200) - expect(json_response["message"]).to eq(broadcast_message.message) + expect(json_response['message']).to eq(broadcast_message.message) end end - context "broadcast message doesn't exist" do - it do - get api("/internal/broadcast_message"), secret_token: secret_token + context 'broadcast message does not exist' do + it 'returns nothing' do + get api('/internal/broadcast_message'), secret_token: secret_token + + expect(response).to have_http_status(200) + expect(json_response).to be_empty + end + end + end + + describe 'GET /internal/broadcast_messages' do + context 'broadcast message(s) exist' do + let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } + + it 'returns active broadcast message(s)' do + get api('/internal/broadcast_messages'), secret_token: secret_token + + expect(response).to have_http_status(200) + expect(json_response[0]['message']).to eq(broadcast_message.message) + end + end + + context 'broadcast message does not exist' do + it 'returns nothing' do + get api('/internal/broadcast_messages'), secret_token: secret_token expect(response).to have_http_status(200) expect(json_response).to be_empty @@ -299,8 +321,6 @@ describe API::Internal do end context "archived project" do - let(:personal_project) { create(:empty_project, namespace: user.namespace) } - before do project.team << [user, :developer] project.archive! @@ -423,6 +443,42 @@ describe API::Internal do expect(json_response['status']).to be_truthy end end + + context 'the project path was changed' do + let!(:old_path_to_repo) { project.repository.path_to_repo } + let!(:old_full_path) { project.full_path } + let(:project_moved_message) do + <<-MSG.strip_heredoc + Project '#{old_full_path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{project.ssh_url_to_repo} + MSG + end + + before do + project.team << [user, :developer] + project.path = 'new_path' + project.save! + end + + it 'rejects the push' do + push_with_path(key, old_path_to_repo) + + expect(response).to have_http_status(200) + expect(json_response['status']).to be_falsey + expect(json_response['message']).to eq(project_moved_message) + end + + it 'rejects the SSH pull' do + pull_with_path(key, old_path_to_repo) + + expect(response).to have_http_status(200) + expect(json_response['status']).to be_falsey + expect(json_response['message']).to eq(project_moved_message) + end + end end describe 'GET /internal/merge_request_urls' do @@ -565,6 +621,17 @@ describe API::Internal do ) end + def pull_with_path(key, path_to_repo, protocol = 'ssh') + post( + api("/internal/allowed"), + key_id: key.id, + project: path_to_repo, + action: 'git-upload-pack', + secret_token: secret_token, + protocol: protocol + ) + end + def push(key, project, protocol = 'ssh', env: nil) post( api("/internal/allowed"), @@ -578,6 +645,19 @@ describe API::Internal do ) end + def push_with_path(key, path_to_repo, protocol = 'ssh', env: nil) + post( + api("/internal/allowed"), + changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master', + key_id: key.id, + project: path_to_repo, + action: 'git-receive-pack', + secret_token: secret_token, + protocol: protocol, + env: env + ) + end + def archive(key, project) post( api("/internal/allowed"), diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index e5e5872dc1f..8d647eb1c7e 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -11,7 +11,7 @@ describe API::Jobs, :api do ref: project.default_branch) end - let!(:build) { create(:ci_build, pipeline: pipeline) } + let!(:job) { create(:ci_build, pipeline: pipeline) } let(:user) { create(:user) } let(:api_user) { user } @@ -42,13 +42,13 @@ describe API::Jobs, :api do end it 'returns pipeline data' do - json_build = json_response.first + json_job = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status + expect(json_job['pipeline']).not_to be_empty + expect(json_job['pipeline']['id']).to eq job.pipeline.id + expect(json_job['pipeline']['ref']).to eq job.pipeline.ref + expect(json_job['pipeline']['sha']).to eq job.pipeline.sha + expect(json_job['pipeline']['status']).to eq job.pipeline.status end context 'filter project with one scope element' do @@ -79,7 +79,7 @@ describe API::Jobs, :api do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return project builds' do + it 'does not return project jobs' do expect(response).to have_http_status(401) end end @@ -105,13 +105,13 @@ describe API::Jobs, :api do end it 'returns pipeline data' do - json_build = json_response.first + json_job = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status + expect(json_job['pipeline']).not_to be_empty + expect(json_job['pipeline']['id']).to eq job.pipeline.id + expect(json_job['pipeline']['ref']).to eq job.pipeline.ref + expect(json_job['pipeline']['sha']).to eq job.pipeline.sha + expect(json_job['pipeline']['status']).to eq job.pipeline.status end context 'filter jobs with one scope element' do @@ -140,7 +140,7 @@ describe API::Jobs, :api do context 'jobs in different pipelines' do let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } - let!(:build2) { create(:ci_build, pipeline: pipeline2) } + let!(:job2) { create(:ci_build, pipeline: pipeline2) } it 'excludes jobs from other pipelines' do json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) } @@ -159,7 +159,7 @@ describe API::Jobs, :api do describe 'GET /projects/:id/jobs/:job_id' do before do - get api("/projects/#{project.id}/jobs/#{build.id}", api_user) + get api("/projects/#{project.id}/jobs/#{job.id}", api_user) end context 'authorized user' do @@ -169,12 +169,13 @@ describe API::Jobs, :api do end it 'returns pipeline data' do - json_build = json_response - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status + json_job = json_response + + expect(json_job['pipeline']).not_to be_empty + expect(json_job['pipeline']['id']).to eq job.pipeline.id + expect(json_job['pipeline']['ref']).to eq job.pipeline.ref + expect(json_job['pipeline']['sha']).to eq job.pipeline.sha + expect(json_job['pipeline']['status']).to eq job.pipeline.status end end @@ -189,11 +190,11 @@ describe API::Jobs, :api do describe 'GET /projects/:id/jobs/:job_id/artifacts' do before do - get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user) + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) end context 'job with artifacts' do - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } context 'authorized user' do let(:download_headers) do @@ -204,7 +205,7 @@ describe API::Jobs, :api do it 'returns specific job artifacts' do expect(response).to have_http_status(200) expect(response.headers).to include(download_headers) - expect(response.body).to match_file(build.artifacts_file.file.file) + expect(response.body).to match_file(job.artifacts_file.file.file) end end @@ -224,14 +225,14 @@ describe API::Jobs, :api do describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do let(:api_user) { reporter } - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } before do - build.success + job.success end - def get_for_ref(ref = pipeline.ref, job = build.name) - get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job + def get_for_ref(ref = pipeline.ref, job_name = job.name) + get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job_name end context 'when not logged in' do @@ -285,7 +286,7 @@ describe API::Jobs, :api do let(:download_headers) do { 'Content-Transfer-Encoding' => 'binary', 'Content-Disposition' => - "attachment; filename=#{build.artifacts_file.filename}" } + "attachment; filename=#{job.artifacts_file.filename}" } end it { expect(response).to have_http_status(200) } @@ -321,16 +322,16 @@ describe API::Jobs, :api do end describe 'GET /projects/:id/jobs/:job_id/trace' do - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } before do - get api("/projects/#{project.id}/jobs/#{build.id}/trace", api_user) + get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user) end context 'authorized user' do it 'returns specific job trace' do expect(response).to have_http_status(200) - expect(response.body).to eq(build.trace.raw) + expect(response.body).to eq(job.trace.raw) end end @@ -345,7 +346,7 @@ describe API::Jobs, :api do describe 'POST /projects/:id/jobs/:job_id/cancel' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/cancel", api_user) + post api("/projects/#{project.id}/jobs/#{job.id}/cancel", api_user) end context 'authorized user' do @@ -375,10 +376,10 @@ describe API::Jobs, :api do end describe 'POST /projects/:id/jobs/:job_id/retry' do - let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + let(:job) { create(:ci_build, :canceled, pipeline: pipeline) } before do - post api("/projects/#{project.id}/jobs/#{build.id}/retry", api_user) + post api("/projects/#{project.id}/jobs/#{job.id}/retry", api_user) end context 'authorized user' do @@ -410,28 +411,29 @@ describe API::Jobs, :api do describe 'POST /projects/:id/jobs/:job_id/erase' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/erase", user) + post api("/projects/#{project.id}/jobs/#{job.id}/erase", user) end context 'job is erasable' do - let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } it 'erases job content' do expect(response).to have_http_status(201) - expect(build).not_to have_trace - expect(build.artifacts_file.exists?).to be_falsy - expect(build.artifacts_metadata.exists?).to be_falsy + expect(job).not_to have_trace + expect(job.artifacts_file.exists?).to be_falsy + expect(job.artifacts_metadata.exists?).to be_falsy end it 'updates job' do - build.reload - expect(build.erased_at).to be_truthy - expect(build.erased_by).to eq(user) + job.reload + + expect(job.erased_at).to be_truthy + expect(job.erased_by).to eq(user) end end context 'job is not erasable' do - let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace, project: project, pipeline: pipeline) } it 'responds with forbidden' do expect(response).to have_http_status(403) @@ -439,25 +441,25 @@ describe API::Jobs, :api do end end - describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do + describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user) + post api("/projects/#{project.id}/jobs/#{job.id}/artifacts/keep", user) end context 'artifacts did not expire' do - let(:build) do + let(:job) do create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) end it 'keeps artifacts' do expect(response).to have_http_status(200) - expect(build.reload.artifacts_expire_at).to be_nil + expect(job.reload.artifacts_expire_at).to be_nil end end context 'no artifacts' do - let(:build) { create(:ci_build, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, project: project, pipeline: pipeline) } it 'responds with not found' do expect(response).to have_http_status(404) @@ -467,18 +469,18 @@ describe API::Jobs, :api do describe 'POST /projects/:id/jobs/:job_id/play' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/play", api_user) + post api("/projects/#{project.id}/jobs/#{job.id}/play", api_user) end context 'on an playable job' do - let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :manual, project: project, pipeline: pipeline) } context 'when user is authorized to trigger a manual action' do it 'plays the job' do expect(response).to have_http_status(200) expect(json_response['user']['id']).to eq(user.id) - expect(json_response['id']).to eq(build.id) - expect(build.reload).to be_pending + expect(json_response['id']).to eq(job.id) + expect(job.reload).to be_pending end end @@ -487,7 +489,7 @@ describe API::Jobs, :api do let(:api_user) { create(:user) } it 'does not trigger a manual action' do - expect(build.reload).to be_manual + expect(job.reload).to be_manual expect(response).to have_http_status(404) end end @@ -496,7 +498,7 @@ describe API::Jobs, :api do let(:api_user) { reporter } it 'does not trigger a manual action' do - expect(build.reload).to be_manual + expect(job.reload).to be_manual expect(response).to have_http_status(403) end end diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb index ab957c72984..f534332ca6c 100644 --- a/spec/requests/api/keys_spec.rb +++ b/spec/requests/api/keys_spec.rb @@ -4,11 +4,9 @@ describe API::Keys do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } - let(:email) { create(:email, user: user) } + let(:email) { create(:email, user: user) } describe 'GET /keys/:uid' do - before { admin } - context 'when unauthenticated' do it 'returns authentication error' do get api("/keys/#{key.id}") diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 0c6b55c1630..f7e2f1908bb 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -339,7 +339,9 @@ describe API::Labels do end context "when user is already subscribed to label" do - before { label1.subscribe(user, project) } + before do + label1.subscribe(user, project) + end it "returns 304" do post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user) @@ -358,7 +360,9 @@ describe API::Labels do end describe "POST /projects/:id/labels/:label_id/unsubscribe" do - before { label1.subscribe(user, project) } + before do + label1.subscribe(user, project) + end context "when label_id is a label title" do it "unsubscribes from the label" do @@ -381,7 +385,9 @@ describe API::Labels do end context "when user is already unsubscribed from label" do - before { label1.unsubscribe(user, project) } + before do + label1.unsubscribe(user, project) + end it "returns 304" do post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 16e5efb2f5b..4d0bd67c571 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -334,14 +334,13 @@ describe API::MergeRequests do target_branch: 'master', author: user, labels: 'label, label2', - milestone_id: milestone.id, - remove_source_branch: true + milestone_id: milestone.id expect(response).to have_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['milestone']['id']).to eq(milestone.id) - expect(json_response['force_remove_source_branch']).to be_truthy + expect(json_response['force_remove_source_branch']).to be_falsy end it "returns 422 when source_branch equals target_branch" do @@ -404,6 +403,27 @@ describe API::MergeRequests do expect(response).to have_http_status(409) end end + + context 'accepts remove_source_branch parameter' do + let(:params) do + { title: 'Test merge_request', + source_branch: 'markdown', + target_branch: 'master', + author: user } + end + + it 'sets force_remove_source_branch to false' do + post api("/projects/#{project.id}/merge_requests", user), params.merge(remove_source_branch: false) + + expect(json_response['force_remove_source_branch']).to be_falsy + end + + it 'sets force_remove_source_branch to true' do + post api("/projects/#{project.id}/merge_requests", user), params.merge(remove_source_branch: true) + + expect(json_response['force_remove_source_branch']).to be_truthy + end + end end context 'forked projects' do @@ -540,8 +560,8 @@ describe API::MergeRequests do end it "returns 406 if branch can't be merged" do - allow_any_instance_of(MergeRequest). - to receive(:can_be_merged?).and_return(false) + allow_any_instance_of(MergeRequest) + .to receive(:can_be_merged?).and_return(false) put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index dd74351a2b1..ab5ea3e8f2c 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -5,8 +5,13 @@ describe API::Milestones do let!(:project) { create(:empty_project, namespace: user.namespace ) } let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } + let(:label_3) { create(:label, title: 'label_3', project: project) } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end describe 'GET /projects/:id/milestones' do it 'returns project milestones' do @@ -226,6 +231,18 @@ describe API::Milestones do expect(json_response.first['milestone']['title']).to eq(milestone.title) end + it 'returns project issues sorted by label priority' do + issue_1 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_3]) + issue_2 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_1]) + issue_3 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_2]) + + get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + + expect(json_response.first['id']).to eq(issue_2.id) + expect(json_response.second['id']).to eq(issue_3.id) + expect(json_response.third['id']).to eq(issue_1.id) + end + it 'matches V4 response schema for a list of issues' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) @@ -242,8 +259,8 @@ describe API::Milestones do describe 'confidential issues' do let(:public_project) { create(:empty_project, :public) } let(:milestone) { create(:milestone, project: public_project) } - let(:issue) { create(:issue, project: public_project, position: 2) } - let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) } + let(:issue) { create(:issue, project: public_project) } + let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } before do public_project.team << [user, :developer] @@ -283,7 +300,10 @@ describe API::Milestones do expect(json_response.map { |issue| issue['id'] }).to include(issue.id) end - it 'returns issues ordered by position asc' do + it 'returns issues ordered by label priority' do + issue.labels << label_2 + confidential_issue.labels << label_1 + get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) expect(response).to have_http_status(200) @@ -297,8 +317,8 @@ describe API::Milestones do end describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do - let(:merge_request) { create(:merge_request, source_project: project, position: 2) } - let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:another_merge_request) { create(:merge_request, :simple, source_project: project) } before do milestone.merge_requests << merge_request @@ -316,6 +336,18 @@ describe API::Milestones do expect(json_response.first['milestone']['title']).to eq(milestone.title) end + it 'returns project merge_requests sorted by label priority' do + merge_request_1 = create(:labeled_merge_request, source_branch: 'branch_1', source_project: project, milestone: milestone, labels: [label_2]) + merge_request_2 = create(:labeled_merge_request, source_branch: 'branch_2', source_project: project, milestone: milestone, labels: [label_1]) + merge_request_3 = create(:labeled_merge_request, source_branch: 'branch_3', source_project: project, milestone: milestone, labels: [label_3]) + + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + + expect(json_response.first['id']).to eq(merge_request_2.id) + expect(json_response.second['id']).to eq(merge_request_1.id) + expect(json_response.third['id']).to eq(merge_request_3.id) + end + it 'returns a 404 error if milestone id not found' do get api("/projects/#{project.id}/milestones/1234/merge_requests", user) @@ -337,6 +369,8 @@ describe API::Milestones do it 'returns merge_requests ordered by position asc' do milestone.merge_requests << another_merge_request + another_merge_request.labels << label_1 + merge_request.labels << label_2 get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 6afcd237c3c..4701ad585c9 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -13,8 +13,8 @@ describe API::Notes do # For testing the cross-reference of a private issue in a public issue let(:private_user) { create(:user) } let(:private_project) do - create(:empty_project, namespace: private_user.namespace). - tap { |p| p.team << [private_user, :master] } + create(:empty_project, namespace: private_user.namespace) + .tap { |p| p.team << [private_user, :master] } end let(:private_issue) { create(:issue, project: private_project) } @@ -28,7 +28,9 @@ describe API::Notes do system: true end - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end describe "GET /projects/:id/noteable/:noteable_id/notes" do context "when noteable is an Issue" do @@ -58,7 +60,9 @@ describe API::Notes do end context "and issue is confidential" do - before { ext_issue.update_attributes(confidential: true) } + before do + ext_issue.update_attributes(confidential: true) + end it "returns 404" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) @@ -150,7 +154,9 @@ describe API::Notes do end context "when issue is confidential" do - before { issue.update_attributes(confidential: true) } + before do + issue.update_attributes(confidential: true) + end it "returns 404" do get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 9e6957e9922..258085e503f 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -10,7 +10,9 @@ describe API::Pipelines do ref: project.default_branch, user: user) end - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end describe 'GET /projects/:id/pipelines ' do context 'authorized user' do @@ -285,7 +287,9 @@ describe API::Pipelines do describe 'POST /projects/:id/pipeline ' do context 'authorized user' do context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } + before do + stub_ci_pipeline_to_return_yaml_file + end it 'creates and returns a new pipeline' do expect do @@ -419,7 +423,9 @@ describe API::Pipelines do context 'user without proper access rights' do let!(:reporter) { create(:user) } - before { project.team << [reporter, :reporter] } + before do + project.team << [reporter, :reporter] + end it 'rejects the action' do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 4d4631322b1..518639f45a2 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -102,23 +102,23 @@ describe API::ProjectSnippets do context 'when the snippet is private' do it 'creates the snippet' do - expect { create_snippet(project, visibility: 'private') }. - to change { Snippet.count }.by(1) + expect { create_snippet(project, visibility: 'private') } + .to change { Snippet.count }.by(1) end end context 'when the snippet is public' do it 'rejects the snippet' do - expect { create_snippet(project, visibility: 'public') }. - not_to change { Snippet.count } + expect { create_snippet(project, visibility: 'public') } + .not_to change { Snippet.count } expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) end it 'creates a spam log' do - expect { create_snippet(project, visibility: 'public') }. - to change { SpamLog.count }.by(1) + expect { create_snippet(project, visibility: 'public') } + .to change { SpamLog.count }.by(1) end end end @@ -166,8 +166,8 @@ describe API::ProjectSnippets do let(:visibility_level) { Snippet::PRIVATE } it 'creates the snippet' do - expect { update_snippet(title: 'Foo') }. - to change { snippet.reload.title }.to('Foo') + expect { update_snippet(title: 'Foo') } + .to change { snippet.reload.title }.to('Foo') end end @@ -175,13 +175,13 @@ describe API::ProjectSnippets do let(:visibility_level) { Snippet::PUBLIC } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo') }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo') } + .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo') }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo') } + .to change { SpamLog.count }.by(1) end end @@ -189,16 +189,16 @@ describe API::ProjectSnippets do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility: 'public') }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo', visibility: 'public') } + .not_to change { snippet.reload.title } expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility: 'public') }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo', visibility: 'public') } + .to change { SpamLog.count }.by(1) end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 3e831373514..fd7ff0b9cff 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -288,15 +288,15 @@ describe API::Projects do context 'maximum number of projects reached' do it 'does not create new project and respond with 403' do allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) - expect { post api('/projects', user2), name: 'foo' }. - to change {Project.count}.by(0) + expect { post api('/projects', user2), name: 'foo' } + .to change {Project.count}.by(0) expect(response).to have_http_status(403) end end it 'creates new project without path but with name and returns 201' do - expect { post api('/projects', user), name: 'Foo Project' }. - to change { Project.count }.by(1) + expect { post api('/projects', user), name: 'Foo Project' } + .to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first @@ -306,8 +306,8 @@ describe API::Projects do end it 'creates new project without name but with path and returns 201' do - expect { post api('/projects', user), path: 'foo_project' }. - to change { Project.count }.by(1) + expect { post api('/projects', user), path: 'foo_project' } + .to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first @@ -317,8 +317,8 @@ describe API::Projects do end it 'creates new project with name and path and returns 201' do - expect { post api('/projects', user), path: 'path-project-Foo', name: 'Foo Project' }. - to change { Project.count }.by(1) + expect { post api('/projects', user), path: 'path-project-Foo', name: 'Foo Project' } + .to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first @@ -476,8 +476,9 @@ describe API::Projects do end describe 'POST /projects/user/:id' do - before { project } - before { admin } + before do + expect(project).to be_persisted + end it 'creates new project without path but with name and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1) @@ -490,8 +491,8 @@ describe API::Projects do end it 'creates new project with name and path and returns 201' do - expect { post api("/projects/user/#{user.id}", admin), path: 'path-project-Foo', name: 'Foo Project' }. - to change { Project.count }.by(1) + expect { post api("/projects/user/#{user.id}", admin), path: 'path-project-Foo', name: 'Foo Project' } + .to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first @@ -501,8 +502,8 @@ describe API::Projects do end it 'responds with 400 on failure and not project' do - expect { post api("/projects/user/#{user.id}", admin) }. - not_to change { Project.count } + expect { post api("/projects/user/#{user.id}", admin) } + .not_to change { Project.count } expect(response).to have_http_status(400) expect(json_response['error']).to eq('name is missing') @@ -581,7 +582,9 @@ describe API::Projects do end describe "POST /projects/:id/uploads" do - before { project } + before do + project + end it "uploads the file and returns its info" do post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @@ -729,14 +732,16 @@ describe API::Projects do describe 'permissions' do context 'all projects' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'contains permission information' do get api("/projects", user) expect(response).to have_http_status(200) - expect(json_response.first['permissions']['project_access']['access_level']). - to eq(Gitlab::Access::MASTER) + expect(json_response.first['permissions']['project_access']['access_level']) + .to eq(Gitlab::Access::MASTER) expect(json_response.first['permissions']['group_access']).to be_nil end end @@ -747,8 +752,8 @@ describe API::Projects do get api("/projects/#{project.id}", user) expect(response).to have_http_status(200) - expect(json_response['permissions']['project_access']['access_level']). - to eq(Gitlab::Access::MASTER) + expect(json_response['permissions']['project_access']['access_level']) + .to eq(Gitlab::Access::MASTER) expect(json_response['permissions']['group_access']).to be_nil end end @@ -756,15 +761,17 @@ describe API::Projects do context 'group project' do let(:project2) { create(:empty_project, group: create(:group)) } - before { project2.group.add_owner(user) } + before do + project2.group.add_owner(user) + end it 'sets the owner and return 200' do get api("/projects/#{project2.id}", user) expect(response).to have_http_status(200) expect(json_response['permissions']['project_access']).to be_nil - expect(json_response['permissions']['group_access']['access_level']). - to eq(Gitlab::Access::OWNER) + expect(json_response['permissions']['group_access']['access_level']) + .to eq(Gitlab::Access::OWNER) end end end @@ -822,7 +829,9 @@ describe API::Projects do end describe 'GET /projects/:id/snippets' do - before { snippet } + before do + snippet + end it 'returns an array of project snippets' do get api("/projects/#{project.id}/snippets", user) @@ -879,7 +888,9 @@ describe API::Projects do end describe 'DELETE /projects/:id/snippets/:snippet_id' do - before { snippet } + before do + snippet + end it 'deletes existing project snippet' do expect do @@ -1074,14 +1085,16 @@ describe API::Projects do end describe 'PUT /projects/:id' do - before { project } - before { user } - before { user3 } - before { user4 } - before { project3 } - before { project4 } - before { project_member2 } - before { project_member } + before do + expect(project).to be_persisted + expect(user).to be_persisted + expect(user3).to be_persisted + expect(user4).to be_persisted + expect(project3).to be_persisted + expect(project4).to be_persisted + expect(project_member2).to be_persisted + expect(project_member).to be_persisted + end it 'returns 400 when nothing sent' do project_param = {} diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 9556c99dea1..339a57a1f20 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -190,17 +190,23 @@ describe API::Runner do pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate") end - before { project.runners << runner } + before do + project.runners << runner + end describe 'POST /api/v4/jobs/request' do let!(:last_update) {} let!(:new_update) { } let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } - before { stub_container_registry_config(enabled: false) } + before do + stub_container_registry_config(enabled: false) + end shared_examples 'no jobs available' do - before { request_job } + before do + request_job + end context 'when runner sends version in User-Agent' do context 'for stable version' do @@ -277,7 +283,9 @@ describe API::Runner do end context 'when jobs are finished' do - before { job.success } + before do + job.success + end it_behaves_like 'no jobs available' end @@ -356,8 +364,11 @@ describe API::Runner do expect(json_response['token']).to eq(job.token) expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) - expect(json_response['image']).to eq({ 'name' => 'ruby:2.1' }) - expect(json_response['services']).to eq([{ 'name' => 'postgres' }]) + expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' }) + expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, + 'alias' => nil, 'command' => nil }, + { 'name' => 'docker:dind', 'entrypoint' => '/bin/sh', + 'alias' => 'docker', 'command' => 'sleep 30' }]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['cache']).to eq(expected_cache) @@ -403,8 +414,8 @@ describe API::Runner do context 'when concurrently updating a job' do before do - expect_any_instance_of(Ci::Build).to receive(:run!). - and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) + expect_any_instance_of(Ci::Build).to receive(:run!) + .and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) end it 'returns a conflict' do @@ -505,10 +516,14 @@ describe API::Runner do end context 'when job has no tags' do - before { job.update(tags: []) } + before do + job.update(tags: []) + end context 'when runner is allowed to pick untagged jobs' do - before { runner.update_column(:run_untagged, true) } + before do + runner.update_column(:run_untagged, true) + end it 'picks job' do request_job @@ -518,7 +533,9 @@ describe API::Runner do end context 'when runner is not allowed to pick untagged jobs' do - before { runner.update_column(:run_untagged, false) } + before do + runner.update_column(:run_untagged, false) + end it_behaves_like 'no jobs available' end @@ -558,7 +575,9 @@ describe API::Runner do end context 'when registry is enabled' do - before { stub_container_registry_config(enabled: true, host_port: registry_url) } + before do + stub_container_registry_config(enabled: true, host_port: registry_url) + end it 'sends registry credentials key' do request_job @@ -569,7 +588,9 @@ describe API::Runner do end context 'when registry is disabled' do - before { stub_container_registry_config(enabled: false, host_port: registry_url) } + before do + stub_container_registry_config(enabled: false, host_port: registry_url) + end it 'does not send registry credentials' do request_job @@ -591,7 +612,9 @@ describe API::Runner do describe 'PUT /api/v4/jobs/:id' do let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } - before { job.run! } + before do + job.run! + end context 'when status is given' do it 'mark job as succeeded' do @@ -646,7 +669,9 @@ describe API::Runner do let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } let(:update_interval) { 10.seconds.to_i } - before { initial_patch_the_trace } + before do + initial_patch_the_trace + end context 'when request is valid' do it 'gets correct response' do @@ -788,7 +813,9 @@ describe API::Runner do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - before { job.run! } + before do + job.run! + end describe 'POST /api/v4/jobs/:id/artifacts/authorize' do context 'when using token as parameter' do @@ -894,13 +921,17 @@ describe API::Runner do end context 'when uses regular file post' do - before { upload_artifacts(file_upload, headers_with_token, false) } + before do + upload_artifacts(file_upload, headers_with_token, false) + end it_behaves_like 'successful artifacts upload' end context 'when uses accelerated file post' do - before { upload_artifacts(file_upload, headers_with_token, true) } + before do + upload_artifacts(file_upload, headers_with_token, true) + end it_behaves_like 'successful artifacts upload' end @@ -1054,7 +1085,9 @@ describe API::Runner do allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) end - after { FileUtils.remove_entry @tmpdir } + after do + FileUtils.remove_entry @tmpdir + end it' "fails to post artifacts for outside of tmp path"' do upload_artifacts(file_upload, headers_with_token) @@ -1076,7 +1109,9 @@ describe API::Runner do describe 'GET /api/v4/jobs/:id/artifacts' do let(:token) { job.token } - before { download_artifact } + before do + download_artifact + end context 'when job has artifacts' do let(:job) { create(:ci_build, :artifacts) } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 2398ae6219c..ede48b1c888 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -40,7 +40,10 @@ describe API::Settings, 'Settings' do plantuml_url: 'http://plantuml.example.com', default_snippet_visibility: 'internal', restricted_visibility_levels: ['public'], - default_artifacts_expire_in: '2 days' + default_artifacts_expire_in: '2 days', + help_page_text: 'custom help text', + help_page_hide_commercial_content: true, + help_page_support_url: 'http://example.com/help' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -53,6 +56,9 @@ describe API::Settings, 'Settings' do expect(json_response['default_snippet_visibility']).to eq('internal') expect(json_response['restricted_visibility_levels']).to eq(['public']) expect(json_response['default_artifacts_expire_in']).to eq('2 days') + expect(json_response['help_page_text']).to eq('custom help text') + expect(json_response['help_page_hide_commercial_content']).to be_truthy + expect(json_response['help_page_support_url']).to eq('http://example.com/help') end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 8741cbd4e80..b20a187acfe 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -142,23 +142,23 @@ describe API::Snippets do context 'when the snippet is private' do it 'creates the snippet' do - expect { create_snippet(visibility: 'private') }. - to change { Snippet.count }.by(1) + expect { create_snippet(visibility: '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: 'public') }. - not_to change { Snippet.count } + expect { create_snippet(visibility: 'public') } + .not_to change { Snippet.count } expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) end it 'creates a spam log' do - expect { create_snippet(visibility: 'public') }. - to change { SpamLog.count }.by(1) + expect { create_snippet(visibility: 'public') } + .to change { SpamLog.count }.by(1) end end end @@ -216,8 +216,8 @@ describe API::Snippets do let(:visibility_level) { Snippet::PRIVATE } it 'updates the snippet' do - expect { update_snippet(title: 'Foo') }. - to change { snippet.reload.title }.to('Foo') + expect { update_snippet(title: 'Foo') } + .to change { snippet.reload.title }.to('Foo') end end @@ -225,16 +225,16 @@ describe API::Snippets do let(:visibility_level) { Snippet::PUBLIC } it 'rejects the shippet' do - expect { update_snippet(title: 'Foo') }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo') } + .not_to change { snippet.reload.title } expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) end it 'creates a spam log' do - expect { update_snippet(title: 'Foo') }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo') } + .to change { SpamLog.count }.by(1) end end @@ -242,13 +242,13 @@ describe API::Snippets do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility: 'public') }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo', visibility: 'public') } + .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility: 'public') }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo', visibility: 'public') } + .to change { SpamLog.count }.by(1) end end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 2eb191d6049..f65b475fe44 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -5,7 +5,9 @@ describe API::SystemHooks do let(:admin) { create(:admin) } let!(:hook) { create(:system_hook, url: "http://example.com") } - before { stub_request(:post, hook.url) } + before do + stub_request(:post, hook.url) + end describe "GET /hooks" do context "when no user" do diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index cb55985e3f5..f8af9295842 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -2,14 +2,18 @@ require 'spec_helper' describe API::Templates do context 'the Template Entity' do - before { get api('/templates/gitignores/Ruby') } + before do + get api('/templates/gitignores/Ruby') + end it { expect(json_response['name']).to eq('Ruby') } it { expect(json_response['content']).to include('*.gem') } end context 'the TemplateList Entity' do - before { get api('/templates/gitignores') } + before do + get api('/templates/gitignores') + end it { expect(json_response.first['name']).not_to be_nil } it { expect(json_response.first['content']).to be_nil } @@ -47,7 +51,9 @@ describe API::Templates do end context 'the License Template Entity' do - before { get api('/templates/licenses/mit') } + before do + get api('/templates/licenses/mit') + end it 'returns a license template' do expect(json_response['key']).to eq('mit') diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index ec51b96c86b..c0174b304c8 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -11,7 +11,7 @@ describe API::Users do let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 } let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 } - describe "GET /users" do + describe 'GET /users' do context "when unauthenticated" do it "returns authentication error" do get api("/users") @@ -76,6 +76,12 @@ describe API::Users do expect(response).to have_http_status(403) end + + it 'does not reveal the `is_admin` flag of the user' do + get api('/users', user) + + expect(json_response.first.keys).not_to include 'is_admin' + end end context "when admin" do @@ -92,6 +98,7 @@ describe API::Users do expect(json_response.first.keys).to include 'two_factor_enabled' expect(json_response.first.keys).to include 'last_sign_in_at' expect(json_response.first.keys).to include 'confirmed_at' + expect(json_response.first.keys).to include 'is_admin' end it "returns an array of external users" do @@ -160,7 +167,9 @@ describe API::Users do end describe "POST /users" do - before { admin } + before do + admin + end it "creates user" do expect do @@ -280,14 +289,14 @@ describe API::Users do bio: 'g' * 256, projects_limit: -1 expect(response).to have_http_status(400) - expect(json_response['message']['password']). - to eq(['is too short (minimum is 8 characters)']) - expect(json_response['message']['bio']). - to eq(['is too long (maximum is 255 characters)']) - expect(json_response['message']['projects_limit']). - to eq(['must be greater than or equal to 0']) - expect(json_response['message']['username']). - to eq([Gitlab::PathRegex.namespace_format_message]) + expect(json_response['message']['password']) + .to eq(['is too short (minimum is 8 characters)']) + expect(json_response['message']['bio']) + .to eq(['is too long (maximum is 255 characters)']) + expect(json_response['message']['projects_limit']) + .to eq(['must be greater than or equal to 0']) + expect(json_response['message']['username']) + .to eq([Gitlab::PathRegex.namespace_format_message]) end it "is not available for non admin users" do @@ -349,10 +358,13 @@ describe API::Users do describe "PUT /users/:id" do let!(:admin_user) { create(:admin) } - before { admin } + before do + admin + end it "updates user with new bio" do put api("/users/#{user.id}", admin), { bio: 'new test bio' } + expect(response).to have_http_status(200) expect(json_response['bio']).to eq('new test bio') expect(user.reload.bio).to eq('new test bio') @@ -373,15 +385,34 @@ describe API::Users do expect(user.reload.organization).to eq('GitLab') end + it 'updates user with avatar' do + put api("/users/#{user.id}", admin), { avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + + user.reload + + expect(user.avatar).to be_present + expect(response).to have_http_status(200) + expect(json_response['avatar_url']).to include(user.avatar_path) + end + it 'updates user with his own email' do put api("/users/#{user.id}", admin), email: user.email + expect(response).to have_http_status(200) expect(json_response['email']).to eq(user.email) expect(user.reload.email).to eq(user.email) end + it 'updates user with a new email' do + put api("/users/#{user.id}", admin), email: 'new@email.com' + + expect(response).to have_http_status(200) + expect(user.reload.notification_email).to eq('new@email.com') + end + it 'updates user with his own username' do put api("/users/#{user.id}", admin), username: user.username + expect(response).to have_http_status(200) expect(json_response['username']).to eq(user.username) expect(user.reload.username).to eq(user.username) @@ -389,12 +420,14 @@ describe API::Users do it "updates user's existing identity" do put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321' + expect(response).to have_http_status(200) expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321') end it 'updates user with new identity' do put api("/users/#{user.id}", admin), provider: 'github', extern_uid: 'john' + expect(response).to have_http_status(200) expect(user.reload.identities.first.extern_uid).to eq('john') expect(user.reload.identities.first.provider).to eq('github') @@ -402,12 +435,14 @@ describe API::Users do it "updates admin status" do put api("/users/#{user.id}", admin), { admin: true } + expect(response).to have_http_status(200) expect(user.reload.admin).to eq(true) end it "updates external status" do put api("/users/#{user.id}", admin), { external: true } + expect(response.status).to eq 200 expect(json_response['external']).to eq(true) expect(user.reload.external?).to be_truthy @@ -415,6 +450,7 @@ describe API::Users do it "does not update admin status" do put api("/users/#{admin_user.id}", admin), { can_create_group: false } + expect(response).to have_http_status(200) expect(admin_user.reload.admin).to eq(true) expect(admin_user.can_create_group).to eq(false) @@ -422,6 +458,7 @@ describe API::Users do it "does not allow invalid update" do put api("/users/#{user.id}", admin), { email: 'invalid email' } + expect(response).to have_http_status(400) expect(user.reload.email).not_to eq('invalid email') end @@ -438,6 +475,7 @@ describe API::Users do it "returns 404 for non-existing user" do put api("/users/999999", admin), { bio: 'update should fail' } + expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -457,14 +495,14 @@ describe API::Users do bio: 'g' * 256, projects_limit: -1 expect(response).to have_http_status(400) - expect(json_response['message']['password']). - to eq(['is too short (minimum is 8 characters)']) - expect(json_response['message']['bio']). - to eq(['is too long (maximum is 255 characters)']) - expect(json_response['message']['projects_limit']). - to eq(['must be greater than or equal to 0']) - expect(json_response['message']['username']). - to eq([Gitlab::PathRegex.namespace_format_message]) + expect(json_response['message']['password']) + .to eq(['is too short (minimum is 8 characters)']) + expect(json_response['message']['bio']) + .to eq(['is too long (maximum is 255 characters)']) + expect(json_response['message']['projects_limit']) + .to eq(['must be greater than or equal to 0']) + expect(json_response['message']['username']) + .to eq([Gitlab::PathRegex.namespace_format_message]) end it 'returns 400 if provider is missing for identity update' do @@ -488,6 +526,7 @@ describe API::Users do it 'returns 409 conflict error if email address exists' do put api("/users/#{@user.id}", admin), email: 'test@example.com' + expect(response).to have_http_status(409) expect(@user.reload.email).to eq(@user.email) end @@ -495,6 +534,7 @@ describe API::Users do it 'returns 409 conflict error if username taken' do @user_id = User.all.last.id put api("/users/#{@user.id}", admin), username: 'test' + expect(response).to have_http_status(409) expect(@user.reload.username).to eq(@user.username) end @@ -502,7 +542,9 @@ describe API::Users do end describe "POST /users/:id/keys" do - before { admin } + before do + admin + end it "does not create invalid ssh key" do post api("/users/#{user.id}/keys", admin), { title: "invalid key" } @@ -532,7 +574,9 @@ describe API::Users do end describe 'GET /user/:id/keys' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -563,7 +607,9 @@ describe API::Users do end describe 'DELETE /user/:id/keys/:key_id' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -601,7 +647,9 @@ describe API::Users do end describe "POST /users/:id/emails" do - before { admin } + before do + admin + end it "does not create invalid email" do post api("/users/#{user.id}/emails", admin), {} @@ -625,7 +673,9 @@ describe API::Users do end describe 'GET /user/:id/emails' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -662,7 +712,9 @@ describe API::Users do end describe 'DELETE /user/:id/emails/:email_id' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -708,7 +760,10 @@ describe API::Users do describe "DELETE /users/:id" do let!(:namespace) { user.namespace } let!(:issue) { create(:issue, author: user) } - before { admin } + + before do + admin + end it "deletes user" do Sidekiq::Testing.inline! { delete api("/users/#{user.id}", admin) } @@ -1068,7 +1123,10 @@ describe API::Users do end describe 'POST /users/:id/block' do - before { admin } + before do + admin + end + it 'blocks existing user' do post api("/users/#{user.id}/block", admin) expect(response).to have_http_status(201) @@ -1096,7 +1154,10 @@ describe API::Users do describe 'POST /users/:id/unblock' do let(:blocked_user) { create(:user, state: 'blocked') } - before { admin } + + before do + admin + end it 'unblocks existing user' do post api("/users/#{user.id}/unblock", admin) diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 378ca1720ff..8b2d165c763 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -126,8 +126,8 @@ describe API::V3::Files do end it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:create_file). - and_raise(Repository::CommitError, 'Cannot create file') + allow_any_instance_of(Repository).to receive(:create_file) + .and_raise(Repository::CommitError, 'Cannot create file') post v3_api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index 98e8c954909..63c5707b2e4 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -505,8 +505,8 @@ describe API::V3::Groups do let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } before(:each) do - allow_any_instance_of(Projects::TransferService). - to receive(:execute).and_return(true) + allow_any_instance_of(Projects::TransferService) + .to receive(:execute).and_return(true) end context "when authenticated as user" do diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index f6ff96be566..4f9e63f2ace 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -432,8 +432,8 @@ describe API::MergeRequests do end it "returns 406 if branch can't be merged" do - allow_any_instance_of(MergeRequest). - to receive(:can_be_merged?).and_return(false) + allow_any_instance_of(MergeRequest) + .to receive(:can_be_merged?).and_return(false) put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb index 2bae4a60931..b5f98a9a545 100644 --- a/spec/requests/api/v3/notes_spec.rb +++ b/spec/requests/api/v3/notes_spec.rb @@ -13,8 +13,8 @@ describe API::V3::Notes do # For testing the cross-reference of a private issue in a public issue let(:private_user) { create(:user) } let(:private_project) do - create(:empty_project, namespace: private_user.namespace). - tap { |p| p.team << [private_user, :master] } + create(:empty_project, namespace: private_user.namespace) + .tap { |p| p.team << [private_user, :master] } end let(:private_issue) { create(:issue, project: private_project) } diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb index 365e7365fda..1950c64c690 100644 --- a/spec/requests/api/v3/project_snippets_spec.rb +++ b/spec/requests/api/v3/project_snippets_spec.rb @@ -85,23 +85,23 @@ describe API::ProjectSnippets 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) + 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 { create_snippet(project, visibility_level: Snippet::PUBLIC) } + .not_to change { Snippet.count } expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) end it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } + .to change { SpamLog.count }.by(1) end end end @@ -147,8 +147,8 @@ describe API::ProjectSnippets do let(:visibility_level) { Snippet::PRIVATE } it 'creates the snippet' do - expect { update_snippet(title: 'Foo') }. - to change { snippet.reload.title }.to('Foo') + expect { update_snippet(title: 'Foo') } + .to change { snippet.reload.title }.to('Foo') end end @@ -156,13 +156,13 @@ describe API::ProjectSnippets do let(:visibility_level) { Snippet::PUBLIC } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo') }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo') } + .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(title: 'Foo') }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo') } + .to change { SpamLog.count }.by(1) end end @@ -170,16 +170,16 @@ describe API::ProjectSnippets do let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. - not_to change { snippet.reload.title } + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } + .not_to change { snippet.reload.title } expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) end it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }. - to change { SpamLog.count }.by(1) + expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } + .to change { SpamLog.count }.by(1) end end end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 47cca4275af..cb74868324c 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -124,6 +124,36 @@ describe API::V3::Projects do end end + context 'and using archived' do + let!(:archived_project) { create(:empty_project, creator_id: user.id, namespace: user.namespace, archived: true) } + + it 'returns archived project' do + get v3_api('/projects?archived=true', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(archived_project.id) + end + + it 'returns non-archived project' do + get v3_api('/projects?archived=false', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(project.id) + end + + it 'returns all project' do + get v3_api('/projects', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + end + end + context 'and using sorting' do before do project2 @@ -301,15 +331,15 @@ describe API::V3::Projects do context 'maximum number of projects reached' do it 'does not create new project and respond with 403' do allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) - expect { post v3_api('/projects', user2), name: 'foo' }. - to change {Project.count}.by(0) + expect { post v3_api('/projects', user2), name: 'foo' } + .to change {Project.count}.by(0) expect(response).to have_http_status(403) end end it 'creates new project without path but with name and returns 201' do - expect { post v3_api('/projects', user), name: 'Foo Project' }. - to change { Project.count }.by(1) + expect { post v3_api('/projects', user), name: 'Foo Project' } + .to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first @@ -319,8 +349,8 @@ describe API::V3::Projects do end it 'creates new project without name but with path and returns 201' do - expect { post v3_api('/projects', user), path: 'foo_project' }. - to change { Project.count }.by(1) + expect { post v3_api('/projects', user), path: 'foo_project' } + .to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first @@ -330,8 +360,8 @@ describe API::V3::Projects do end it 'creates new project name and path and returns 201' do - expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }. - to change { Project.count }.by(1) + expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' } + .to change { Project.count }.by(1) expect(response).to have_http_status(201) project = Project.first @@ -489,8 +519,8 @@ describe API::V3::Projects do end it 'responds with 400 on failure and not project' do - expect { post v3_api("/projects/user/#{user.id}", admin) }. - not_to change { Project.count } + expect { post v3_api("/projects/user/#{user.id}", admin) } + .not_to change { Project.count } expect(response).to have_http_status(400) expect(json_response['error']).to eq('name is missing') @@ -716,8 +746,8 @@ describe API::V3::Projects do get v3_api("/projects", user) expect(response).to have_http_status(200) - expect(json_response.first['permissions']['project_access']['access_level']). - to eq(Gitlab::Access::MASTER) + expect(json_response.first['permissions']['project_access']['access_level']) + .to eq(Gitlab::Access::MASTER) expect(json_response.first['permissions']['group_access']).to be_nil end end @@ -728,8 +758,8 @@ describe API::V3::Projects do get v3_api("/projects/#{project.id}", user) expect(response).to have_http_status(200) - expect(json_response['permissions']['project_access']['access_level']). - to eq(Gitlab::Access::MASTER) + expect(json_response['permissions']['project_access']['access_level']) + .to eq(Gitlab::Access::MASTER) expect(json_response['permissions']['group_access']).to be_nil end end @@ -744,8 +774,8 @@ describe API::V3::Projects do expect(response).to have_http_status(200) expect(json_response['permissions']['project_access']).to be_nil - expect(json_response['permissions']['group_access']['access_level']). - to eq(Gitlab::Access::OWNER) + expect(json_response['permissions']['group_access']['access_level']) + .to eq(Gitlab::Access::OWNER) end end end diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb index 4f02b7b1a54..1bc2258ebd3 100644 --- a/spec/requests/api/v3/snippets_spec.rb +++ b/spec/requests/api/v3/snippets_spec.rb @@ -112,21 +112,21 @@ describe API::V3::Snippets do 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) + 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 { 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) + expect { create_snippet(visibility_level: Snippet::PUBLIC) } + .to change { SpamLog.count }.by(1) end end end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index e9c57f7c6c3..6d7401f9764 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -7,6 +7,38 @@ describe API::V3::Users do let(:email) { create(:email, user: user) } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + describe 'GET /users' do + context 'when authenticated' do + it 'returns an array of users' do + get v3_api('/users', user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + username = user.username + expect(json_response.detect do |user| + user['username'] == username + end['username']).to eq(username) + end + end + + context 'when authenticated as user' do + it 'does not reveal the `is_admin` flag of the user' do + get v3_api('/users', user) + + expect(json_response.first.keys).not_to include 'is_admin' + end + end + + context 'when authenticated as admin' do + it 'reveals the `is_admin` flag of the user' do + get v3_api('/users', admin) + + expect(json_response.first.keys).to include 'is_admin' + end + end + end + describe 'GET /user/:id/keys' do before { admin } diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 83673864fe7..e0975024b80 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -82,6 +82,17 @@ describe API::Variables do expect(json_response['protected']).to be_truthy end + it 'creates variable with optional attributes' do + expect do + post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' + end.to change{project.variables.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response['key']).to eq('TEST_VARIABLE_2') + expect(json_response['value']).to eq('VALUE_2') + expect(json_response['protected']).to be_falsey + end + it 'does not allow to duplicate variable key' do expect do post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2' diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 286de277ae7..c969d08d0dd 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -91,8 +91,8 @@ describe Ci::API::Builds do context 'when concurrently updating build' do before do - expect_any_instance_of(Ci::Build).to receive(:run!). - and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) + expect_any_instance_of(Ci::Build).to receive(:run!) + .and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) end it 'returns a conflict' do @@ -137,6 +137,18 @@ describe Ci::API::Builds do end end end + + context 'when docker configuration options are used' do + let!(:build) { create(:ci_build, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + + it 'starts a build' do + register_builds info: { platform: :darwin } + + expect(response).to have_http_status(201) + expect(json_response['options']['image']).to eq('ruby:2.1') + expect(json_response['options']['services']).to eq(['postgres', 'docker:dind']) + end + end end context 'when builds are finished' do @@ -229,7 +241,9 @@ describe Ci::API::Builds do end context 'when runner is allowed to pick untagged builds' do - before { runner.update_column(:run_untagged, true) } + before do + runner.update_column(:run_untagged, true) + end it 'picks build' do register_builds @@ -455,7 +469,9 @@ describe Ci::API::Builds do let(:token) { build.token } let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) } - before { build.run! } + before do + build.run! + end describe "POST /builds/:id/artifacts/authorize" do context "authorizes posting artifact to running build" do @@ -511,7 +527,9 @@ describe Ci::API::Builds do end context 'authorization token is invalid' do - before { post authorize_url, { token: 'invalid', filesize: 100 } } + before do + post authorize_url, { token: 'invalid', filesize: 100 } + end it 'responds with forbidden' do expect(response).to have_http_status(403) @@ -652,8 +670,8 @@ describe Ci::API::Builds do build.reload expect(response).to have_http_status(201) expect(json_response['artifacts_expire_at']).not_to be_empty - expect(build.artifacts_expire_at). - to be_within(5.minutes).of(7.days.from_now) + expect(build.artifacts_expire_at) + .to be_within(5.minutes).of(7.days.from_now) end end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 0b9733221d8..78b2be350cd 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -12,7 +12,9 @@ describe Ci::API::Runners do describe "POST /runners/register" do context 'when runner token is provided' do - before { post ci_api("/runners/register"), token: registration_token } + before do + post ci_api("/runners/register"), token: registration_token + end it 'creates runner with default values' do expect(response).to have_http_status 201 @@ -69,7 +71,10 @@ describe Ci::API::Runners do context 'when project token is provided' do let(:project) { FactoryGirl.create(:empty_project) } - before { post ci_api("/runners/register"), token: project.runners_token } + + before do + post ci_api("/runners/register"), token: project.runners_token + end it 'creates runner' do expect(response).to have_http_status 201 diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 6a83024d0d5..185679e1a0f 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -316,6 +316,26 @@ describe 'Git HTTP requests', lib: true do it_behaves_like 'pushes require Basic HTTP Authentication' end end + + context 'and the user requests a redirected path' do + let!(:redirect) { project.route.create_redirect('foo/bar') } + let(:path) { "#{redirect.path}.git" } + let(:project_moved_message) do + <<-MSG.strip_heredoc + Project '#{redirect.path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{project.http_url_to_repo} + MSG + end + + it 'downloads get status 404 with "project was moved" message' do + clone_get(path, {}) + expect(response).to have_http_status(:not_found) + expect(response.body).to match(project_moved_message) + end + end end context "when the project is private" do @@ -463,8 +483,8 @@ describe 'Git HTTP requests', lib: true do context 'when LDAP is configured' do before do allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) - allow_any_instance_of(Gitlab::LDAP::Authentication). - to receive(:login).and_return(nil) + allow_any_instance_of(Gitlab::LDAP::Authentication) + .to receive(:login).and_return(nil) end it 'does not display the personal access token error message' do @@ -505,6 +525,33 @@ describe 'Git HTTP requests', lib: true do Rack::Attack::Allow2Ban.reset(ip, options) end end + + context 'and the user requests a redirected path' do + let!(:redirect) { project.route.create_redirect('foo/bar') } + let(:path) { "#{redirect.path}.git" } + let(:project_moved_message) do + <<-MSG.strip_heredoc + Project '#{redirect.path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{project.http_url_to_repo} + MSG + end + + it 'downloads get status 404 with "project was moved" message' do + clone_get(path, env) + expect(response).to have_http_status(:not_found) + expect(response.body).to match(project_moved_message) + end + + it 'uploads get status 404 with "project was moved" message' do + upload(path, env) do |response| + expect(response).to have_http_status(:not_found) + expect(response.body).to match(project_moved_message) + end + end + end end context "when the user doesn't have access to the project" do @@ -627,7 +674,9 @@ describe 'Git HTTP requests', lib: true do let(:path) { "/#{project.path_with_namespace}/info/refs" } context "when no params are added" do - before { get path } + before do + get path + end it "redirects to the .git suffix version" do expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs") @@ -636,7 +685,10 @@ describe 'Git HTTP requests', lib: true do context "when the upload-pack service is requested" do let(:params) { { service: 'git-upload-pack' } } - before { get path, params } + + before do + get path, params + end it "redirects to the .git suffix version" do expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") @@ -645,7 +697,10 @@ describe 'Git HTTP requests', lib: true do context "when the receive-pack service is requested" do let(:params) { { service: 'git-receive-pack' } } - before { get path, params } + + before do + get path, params + end it "redirects to the .git suffix version" do expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") @@ -654,7 +709,10 @@ describe 'Git HTTP requests', lib: true do context "when the params are anything else" do let(:params) { { service: 'git-implode-pack' } } - before { get path, params } + + before do + get path, params + end it "redirects to the sign-in page" do expect(response).to redirect_to(new_user_session_path) @@ -669,7 +727,7 @@ describe 'Git HTTP requests', lib: true do end context "POST git-receive-pack" do - it "failes to find a route" do + it "fails to find a route" do expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) end end @@ -695,7 +753,9 @@ describe 'Git HTTP requests', lib: true do end context "when the file does not exist" do - before { get "/#{project.path_with_namespace}/blob/master/info/refs" } + before do + get "/#{project.path_with_namespace}/blob/master/info/refs" + end it "returns not found" do expect(response).to have_http_status(:not_found) diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 54d7cf5f10d..5e4cf05748e 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -6,7 +6,9 @@ describe JwtController do let(:service_name) { 'test' } let(:parameters) { { service: service_name } } - before { stub_const('JwtController::SERVICES', service_name => service_class) } + before do + stub_const('JwtController::SERVICES', service_name => service_class) + end context 'existing service' do subject! { get '/jwt/auth', parameters } diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 0a6778ae2ef..95d40138fea 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -93,13 +93,17 @@ describe 'project routing' do end context 'name with dot' do - before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys', any_args).and_return(true) } + before do + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys', any_args).and_return(true) + end it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') } end context 'with nested group' do - before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq', any_args).and_return(true) } + before do + allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq', any_args).and_return(true) + end it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') } end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index a62af13cf0c..a45839b16f5 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -286,7 +286,9 @@ end describe "Groups", "routing" do let(:name) { 'complex.group-namegit' } - before { allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true) } + before do + allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true) + end it "to #show" do expect(get("/groups/#{name}")).to route_to('groups#show', id: name) diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb index 968dcd6232e..38b8f439a55 100644 --- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb +++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb @@ -54,8 +54,8 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do aggregate_failures do expect(cop.offenses.size).to eq(1) expect(cop.offenses.map(&:line)).to eq([2]) - expect(cop.offenses.first.message). - to include("`#{relative_spec_filepath}`") + expect(cop.offenses.first.message) + .to include("`#{relative_spec_filepath}`") end end end diff --git a/spec/rubocop/cop/rspec/single_line_hook_spec.rb b/spec/rubocop/cop/rspec/single_line_hook_spec.rb new file mode 100644 index 00000000000..6cf0831d3ad --- /dev/null +++ b/spec/rubocop/cop/rspec/single_line_hook_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/rspec/single_line_hook' + +describe RuboCop::Cop::RSpec::SingleLineHook do + include CopHelper + + subject(:cop) { described_class.new } + + # Override `CopHelper#inspect_source` to always appear to be in a spec file, + # so that our RSpec-only cop actually runs + def inspect_source(*args) + super(*args, 'foo_spec.rb') + end + + it 'registers an offense for a single-line `before` block' do + inspect_source(cop, 'before { do_something }') + + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['before { do_something }']) + end + + it 'registers an offense for a single-line `after` block' do + inspect_source(cop, 'after(:each) { undo_something }') + + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['after(:each) { undo_something }']) + end + + it 'registers an offense for a single-line `around` block' do + inspect_source(cop, 'around { |ex| do_something_else }') + + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['around { |ex| do_something_else }']) + end + + it 'ignores a multi-line `before` block' do + inspect_source(cop, ['before do', + ' do_something', + 'end']) + + expect(cop.offenses.size).to eq(0) + end + + it 'ignores a multi-line `after` block' do + inspect_source(cop, ['after(:each) do', + ' undo_something', + 'end']) + + expect(cop.offenses.size).to eq(0) + end + + it 'ignores a multi-line `around` block' do + inspect_source(cop, ['around do |ex|', + ' do_something_else', + 'end']) + + expect(cop.offenses.size).to eq(0) + end +end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 396ba96e9b3..b92c1c28ba8 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe BuildDetailsEntity do set(:user) { create(:admin) } - it 'inherits from BuildEntity' do - expect(described_class).to be < BuildEntity + it 'inherits from JobEntity' do + expect(described_class).to be < JobEntity end describe '#as_json' do diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index d2ad6c44702..4c52a00b442 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -62,7 +62,9 @@ describe EnvironmentSerializer do subject { serializer.represent(resource) } context 'when there is a single environment' do - before { create(:environment, name: 'staging') } + before do + create(:environment, name: 'staging') + end it 'represents one standalone environment' do expect(subject.count).to eq 1 @@ -138,7 +140,9 @@ describe EnvironmentSerializer do context 'when resource is paginatable relation' do context 'when there is a single environment object in relation' do - before { create(:environment) } + before do + create(:environment) + end it 'serializes environments' do expect(subject.first).to have_key :id @@ -146,7 +150,9 @@ describe EnvironmentSerializer do end context 'when multiple environment objects are serialized' do - before { create_list(:environment, 3) } + before do + create_list(:environment, 3) + end it 'serializes appropriate number of objects' do expect(subject.count).to be 2 diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/job_entity_spec.rb index e51ff9fc709..5ca7bf2fcaf 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe BuildEntity do +describe JobEntity do let(:user) { create(:user) } - let(:build) { create(:ci_build) } - let(:project) { build.project } + let(:job) { create(:ci_build) } + let(:project) { job.project } let(:request) { double('request') } before do @@ -12,12 +12,12 @@ describe BuildEntity do end let(:entity) do - described_class.new(build, request: request) + described_class.new(job, request: request) end subject { entity.as_json } - it 'contains paths to build page action' do + it 'contains paths to job page action' do expect(subject).to include(:build_path) end @@ -27,7 +27,7 @@ describe BuildEntity do end it 'contains whether it is playable' do - expect(subject[:playable]).to eq build.playable? + expect(subject[:playable]).to eq job.playable? end it 'contains timestamps' do @@ -39,9 +39,9 @@ describe BuildEntity do expect(subject[:status]).to include :icon, :favicon, :text, :label end - context 'when build is retryable' do + context 'when job is retryable' do before do - build.update(status: :failed) + job.update(status: :failed) end it 'contains cancel path' do @@ -49,9 +49,9 @@ describe BuildEntity do end end - context 'when build is cancelable' do + context 'when job is cancelable' do before do - build.update(status: :running) + job.update(status: :running) end it 'contains cancel path' do @@ -59,7 +59,7 @@ describe BuildEntity do end end - context 'when build is a regular build' do + context 'when job is a regular job' do it 'does not contain path to play action' do expect(subject).not_to include(:play_path) end @@ -69,8 +69,8 @@ describe BuildEntity do end end - context 'when build is a manual action' do - let(:build) { create(:ci_build, :manual) } + context 'when job is a manual action' do + let(:job) { create(:ci_build, :manual) } context 'when user is allowed to trigger action' do before do @@ -99,4 +99,25 @@ describe BuildEntity do end end end + + context 'when job is generic commit status' do + let(:job) { create(:generic_commit_status, target_url: 'http://google.com') } + + it 'contains paths to target action' do + expect(subject).to include(:build_path) + end + + it 'does not contain paths to other action paths' do + expect(subject).not_to include(:retry_path, :cancel_path, :play_path) + end + + it 'contains timestamps' do + expect(subject).to include(:created_at, :updated_at) + end + + it 'contains details' do + expect(subject).to include :status + expect(subject[:status]).to include :icon, :favicon, :text, :label + end + end end diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index 03cc5ae9b63..d28dec9592a 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -51,7 +51,9 @@ describe PipelineDetailsEntity do end context 'user has ability to retry pipeline' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'retryable flag is true' do expect(subject[:flags][:retryable]).to eq true @@ -77,7 +79,9 @@ describe PipelineDetailsEntity do end context 'user has ability to cancel pipeline' do - before { project.add_developer(user) } + before do + project.add_developer(user) + end it 'cancelable flag is true' do expect(subject[:flags][:cancelable]).to eq true @@ -91,6 +95,20 @@ describe PipelineDetailsEntity do end end + context 'when pipeline has commit statuses' do + let(:pipeline) { create(:ci_empty_pipeline) } + + before do + create(:generic_commit_status, pipeline: pipeline) + end + + it 'contains stages' do + expect(subject).to include(:details) + expect(subject[:details]).to include(:stages) + expect(subject[:details][:stages].first).to include(name: 'external') + end + end + context 'when pipeline has YAML errors' do let(:pipeline) do create(:ci_pipeline, config: { rspec: { invalid: :value } }) diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index a059c2cc736..46650f3a80d 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -51,7 +51,9 @@ describe PipelineEntity do end context 'user has ability to retry pipeline' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'contains retry path' do expect(subject[:retry_path]).to be_present @@ -77,7 +79,9 @@ describe PipelineEntity do end context 'user has ability to cancel pipeline' do - before { project.add_developer(user) } + before do + project.add_developer(user) + end it 'contains cancel path' do expect(subject[:cancel_path]).to be_present diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 34b19fb9fc4..44813656aff 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -69,7 +69,9 @@ describe PipelineSerializer do let(:pagination) { { page: 1, per_page: 2 } } context 'when a single pipeline object is present in relation' do - before { create(:ci_empty_pipeline) } + before do + create(:ci_empty_pipeline) + end it 'serializes pipeline relation' do expect(subject.first).to have_key :id @@ -77,7 +79,9 @@ describe PipelineSerializer do end context 'when a multiple pipeline objects are being serialized' do - before { create_list(:ci_empty_pipeline, 3) } + before do + create_list(:ci_empty_pipeline, 3) + end it 'serializes appropriate number of objects' do expect(subject.count).to be 2 diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb index 64b3217b809..40e303f7b89 100644 --- a/spec/serializers/stage_entity_spec.rb +++ b/spec/serializers/stage_entity_spec.rb @@ -54,6 +54,17 @@ describe StageEntity do it 'exposes the group key' do expect(subject).to include :groups end + + context 'and contains commit status' do + before do + create(:generic_commit_status, pipeline: pipeline, stage: 'test') + end + + it 'contains commit status' do + groups = subject[:groups].map { |group| group[:name] } + expect(groups).to include('generic') + end + end end end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index e273dfe1552..60cb7a9440f 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -34,7 +34,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for changed configuration' do - before { stub_application_setting(container_registry_token_expire_delay: expire_delay) } + before do + stub_application_setting(container_registry_token_expire_delay: expire_delay) + end it { expect(expires_at).to be_within(2.seconds).of(Time.now + expire_delay.minutes) } end @@ -117,7 +119,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'allow developer to push images' do - before { project.team << [current_user, :developer] } + before do + project.team << [current_user, :developer] + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } @@ -128,7 +132,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'allow reporter to pull images' do - before { project.team << [current_user, :reporter] } + before do + project.team << [current_user, :reporter] + end context 'when pulling from root level repository' do let(:current_params) do @@ -141,7 +147,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'return a least of privileges' do - before { project.team << [current_user, :reporter] } + before do + project.team << [current_user, :reporter] + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push,pull" } @@ -152,7 +160,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'disallow guest to pull or push images' do - before { project.team << [current_user, :guest] } + before do + project.team << [current_user, :guest] + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull,push" } @@ -355,7 +365,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'for project without container registry' do let(:project) { create(:empty_project, :public, container_registry_enabled: false) } - before { project.update(container_registry_enabled: false) } + before do + project.update(container_registry_enabled: false) + end context 'disallow when pulling' do let(:current_params) do diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index a1e220c2322..a66cc2cd6e9 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -67,7 +67,7 @@ describe Boards::Issues::ListService, services: true do issues = described_class.new(project, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1] end it 'returns opened issues that have label list applied when listing issues from a label list' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 1557cb3c938..efcaccc254e 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -62,6 +62,10 @@ describe Ci::ProcessPipelineService, '#execute', :services do fail_running_or_pending expect(builds_statuses).to eq %w(failed pending) + + fail_running_or_pending + + expect(pipeline.reload).to be_success end end diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index c44e6b2a48b..efefa8e8eca 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -9,7 +9,9 @@ describe Ci::UpdateBuildQueueService, :services do let(:runner) { create(:ci_runner) } context 'when there are runner that can pick build' do - before { build.project.runners << runner } + before do + build.project.runners << runner + end it 'ticks runner queue value' do expect { subject.execute(build) } @@ -36,7 +38,9 @@ describe Ci::UpdateBuildQueueService, :services do end context 'when there are no runners that can pick build' do - before { build.tag_list = [:docker] } + before do + build.tag_list = [:docker] + end it 'does not tick runner queue value' do expect { subject.execute(build) } diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 5398b5c3f7e..dfab6ebf372 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -122,6 +122,61 @@ describe CreateDeploymentService, services: true do end end + describe '#expanded_environment_url' do + subject { service.send(:expanded_environment_url) } + + context 'when yaml environment uses $CI_COMMIT_REF_NAME' do + let(:job) do + create(:ci_build, + ref: 'master', + options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } }) + end + + it { is_expected.to eq('http://review/master') } + end + + context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'production', + options: { environment: { url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) + end + + let!(:environment) do + create(:environment, + project: job.project, + name: 'production', + slug: 'prod-slug', + external_url: 'http://review/old') + end + + it { is_expected.to eq('http://review/prod-slug') } + end + + context 'when yaml environment uses yaml_variables containing symbol keys' do + let(:job) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + options: { environment: { url: 'http://review/$APP_HOST' } }) + end + + it { is_expected.to eq('http://review/host') } + end + + context 'when yaml environment does not have url' do + let(:job) { create(:ci_build, environment: 'staging') } + + let!(:environment) do + create(:environment, project: job.project, name: job.environment) + end + + it 'returns the external_url from persisted environment' do + is_expected.to be_nil + end + end + end + describe 'processing of builds' do shared_examples 'does not create deployment' do it 'does not create a new deployment' do @@ -204,7 +259,9 @@ describe CreateDeploymentService, services: true do let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } context "while updating the 'first_deployed_to_production_at' time" do - before { merge_request.mark_as_merged } + before do + merge_request.mark_as_merged + end context "for merge requests merged before the current deploy" do it "sets the time if the deploy's environment is 'production'" do diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb new file mode 100644 index 00000000000..c1f477f551e --- /dev/null +++ b/spec/services/emails/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Emails::CreateService, services: true do + let(:user) { create(:user) } + let(:opts) { { email: 'new@email.com' } } + + subject(:service) { described_class.new(user, opts) } + + describe '#execute' do + it 'creates an email with valid attributes' do + expect { service.execute }.to change { Email.count }.by(1) + expect(Email.where(opts)).not_to be_empty + end + + it 'has the right user association' do + service.execute + + expect(user.emails).to eq(Email.where(opts)) + end + end +end diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb new file mode 100644 index 00000000000..5e7ab4a40af --- /dev/null +++ b/spec/services/emails/destroy_service_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Emails::DestroyService, services: true do + let!(:user) { create(:user) } + let!(:email) { create(:email, user: user) } + + subject(:service) { described_class.new(user, email: email.email) } + + describe '#execute' do + it 'removes an email' do + expect { service.execute }.to change { user.emails.count }.by(-1) + end + end +end diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index 16bca66766a..cc950ae6bb3 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -32,8 +32,8 @@ describe Files::UpdateService do let(:last_commit_sha) { "foo" } it "returns a hash with the correct error message and a :error status " do - expect { subject.execute }. - to raise_error(Files::UpdateService::FileChangedError, + expect { subject.execute } + .to raise_error(Files::UpdateService::FileChangedError, "You are attempting to update a file that has changed since you started editing it.") end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index bcd1fb64ab9..ca827fc0f39 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -158,8 +158,8 @@ describe GitPushService, services: true do context "Updates merge requests" do it "when pushing a new branch for the first time" do - expect(UpdateMergeRequestsWorker).to receive(:perform_async). - with(project.id, user.id, @blankrev, 'newrev', 'refs/heads/master') + expect(UpdateMergeRequestsWorker).to receive(:perform_async) + .with(project.id, user.id, @blankrev, 'newrev', 'refs/heads/master') execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end end @@ -283,8 +283,8 @@ describe GitPushService, services: true do author_email: commit_author.email ) - allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). - and_return(commit) + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit) + .and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) end @@ -341,8 +341,8 @@ describe GitPushService, services: true do committed_date: commit_time ) - allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). - and_return(commit) + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit) + .and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) end @@ -377,11 +377,11 @@ describe GitPushService, services: true do author_email: commit_author.email ) - allow(project.repository).to receive(:commits_between). - and_return([closing_commit]) + allow(project.repository).to receive(:commits_between) + .and_return([closing_commit]) - allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). - and_return(closing_commit) + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit) + .and_return(closing_commit) project.team << [commit_author, :master] end @@ -403,8 +403,8 @@ describe GitPushService, services: true do end it "doesn't close issues when external issue tracker is in use" do - allow_any_instance_of(Project).to receive(:default_issues_tracker?). - and_return(false) + allow_any_instance_of(Project).to receive(:default_issues_tracker?) + .and_return(false) external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern) allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker) @@ -598,13 +598,13 @@ describe GitPushService, services: true do commit = double(:commit) diff = double(:diff, new_path: 'README.md') - expect(commit).to receive(:raw_deltas). - and_return([diff]) + expect(commit).to receive(:raw_deltas) + .and_return([diff]) service.push_commits = [commit] - expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, %i(readme), %i(commit_count repository_size)) + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, %i(readme), %i(commit_count repository_size)) service.update_caches end @@ -616,9 +616,9 @@ describe GitPushService, services: true do end it 'does not flush any conditional caches' do - expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, [], %i(commit_count repository_size)). - and_call_original + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], %i(commit_count repository_size)) + .and_call_original service.update_caches end @@ -635,8 +635,8 @@ describe GitPushService, services: true do end it 'only schedules a limited number of commits' do - allow(service).to receive(:push_commits). - and_return(Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true))) + allow(service).to receive(:push_commits) + .and_return(Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true))) expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times @@ -644,8 +644,8 @@ describe GitPushService, services: true do end it "skips commits which don't include cross-references" do - allow(service).to receive(:push_commits). - and_return([double(:commit, to_hash: {}, matches_cross_reference_regex?: false)]) + allow(service).to receive(:push_commits) + .and_return([double(:commit, to_hash: {}, matches_cross_reference_regex?: false)]) expect(ProcessCommitWorker).not_to receive(:perform_async) diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index bcb62429275..fbd9026640c 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -14,7 +14,9 @@ describe Groups::CreateService, '#execute', services: true do end context "cannot create group with restricted visibility level" do - before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) + end it { is_expected.not_to be_persisted } end @@ -25,7 +27,9 @@ describe Groups::CreateService, '#execute', services: true do let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } context 'as group owner' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it { is_expected.to be_persisted } end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index be0e829880e..d6f4c694069 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -18,26 +18,26 @@ describe Issues::CloseService, services: true do let(:service) { described_class.new(project, user) } it 'checks if the user is authorized to update the issue' do - expect(service).to receive(:can?).with(user, :update_issue, issue). - and_call_original + expect(service).to receive(:can?).with(user, :update_issue, issue) + .and_call_original service.execute(issue) end it 'does not close the issue when the user is not authorized to do so' do - allow(service).to receive(:can?).with(user, :update_issue, issue). - and_return(false) + allow(service).to receive(:can?).with(user, :update_issue, issue) + .and_return(false) expect(service).not_to receive(:close_issue) expect(service.execute(issue)).to eq(issue) end it 'closes the issue when the user is authorized to do so' do - allow(service).to receive(:can?).with(user, :update_issue, issue). - and_return(true) + allow(service).to receive(:can?).with(user, :update_issue, issue) + .and_return(true) - expect(service).to receive(:close_issue). - with(issue, commit: nil, notifications: true, system_note: true) + expect(service).to receive(:close_issue) + .with(issue, commit: nil, notifications: true, system_note: true) service.execute(issue) end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index dab1a3469f7..ae9d2b2855d 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -155,7 +155,9 @@ describe Issues::CreateService, services: true do context 'issue create service' do context 'assignees' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'removes assignee when user id is invalid' do opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } @@ -204,9 +206,9 @@ describe Issues::CreateService, services: true do end end - it_behaves_like 'new issuable record that supports slash commands' + it_behaves_like 'new issuable record that supports quick actions' - context 'Slash commands' do + context 'Quick actions' do context 'with assignee and milestone in params and command' do let(:opts) do { diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 9f8346d52bb..d1dd1466d95 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -251,12 +251,18 @@ describe Issues::MoveService, services: true do end context 'user is reporter only in new project' do - before { new_project.team << [user, :reporter] } + before do + new_project.team << [user, :reporter] + end + it { expect { move }.to raise_error(StandardError, /permissions/) } end context 'user is reporter only in old project' do - before { old_project.team << [user, :reporter] } + before do + old_project.team << [user, :reporter] + end + it { expect { move }.to raise_error(StandardError, /permissions/) } end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index a78866a2c32..c26642f5015 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -295,7 +295,9 @@ describe Issues::UpdateService, services: true do end context 'when issue has the `label` label' do - before { issue.labels << label } + before do + issue.labels << label + end it 'does not send notifications for existing labels' do opts = { label_ids: [label.id, label2.id] } @@ -329,7 +331,9 @@ describe Issues::UpdateService, services: true do it { expect(issue.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_issue(description: "- [x] Task 1\n- [X] Task 2") } + before do + update_issue(description: "- [x] Task 1\n- [X] Task 2") + end it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') @@ -417,7 +421,9 @@ describe Issues::UpdateService, services: true do context 'when remove_label_ids and label_ids are passed' do let(:params) { { label_ids: [], remove_label_ids: [label.id] } } - before { issue.update_attributes(labels: [label, label3]) } + before do + issue.update_attributes(labels: [label, label3]) + end it 'ignores the label_ids parameter' do expect(result.label_ids).not_to be_empty @@ -431,7 +437,9 @@ describe Issues::UpdateService, services: true do context 'when add_label_ids and remove_label_ids are passed' do let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } } - before { issue.update_attributes(labels: [label]) } + before do + issue.update_attributes(labels: [label]) + end it 'adds the passed labels' do expect(result.label_ids).to include(label3.id) diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb index 4b90ad19640..500afdfb916 100644 --- a/spec/services/labels/promote_service_spec.rb +++ b/spec/services/labels/promote_service_spec.rb @@ -66,9 +66,9 @@ describe Labels::PromoteService, services: true do 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 { 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 diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 5ce8e17976b..5a05ab3ea50 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -5,7 +5,9 @@ describe Members::CreateService, services: true do let(:user) { create(:user) } let(:project_user) { create(:user) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'adds user to members' do params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST } diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 41450c67d7e..9ab7839430c 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -104,8 +104,8 @@ describe Members::DestroyService, services: true do let(:params) { { id: project.members.find_by!(user_id: user.id).id } } it 'destroys the member' do - expect { described_class.new(project, user, params).execute }. - to change { project.members.count }.by(-1) + expect { described_class.new(project, user, params).execute } + .to change { project.members.count }.by(-1) end end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 6f9d1208b1d..01ef52396d7 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -206,7 +206,9 @@ describe MergeRequests::BuildService, services: true do context 'branch starts with external issue IID followed by a hyphen' do let(:source_branch) { '12345-fix-issue' } - before { allow(project).to receive(:default_issues_tracker?).and_return(false) } + before do + allow(project).to receive(:default_issues_tracker?).and_return(false) + end it 'sets the title to: Resolves External Issue $issue-iid' do expect(merge_request.title).to eq('Resolve External Issue 12345') diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 154f30aac3b..074d4672b06 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -32,8 +32,8 @@ describe MergeRequests::CloseService, services: true do it { expect(@merge_request).to be_closed } it 'executes hooks with close action' do - expect(service).to have_received(:execute_hooks). - with(@merge_request, 'close') + expect(service).to have_received(:execute_hooks) + .with(@merge_request, 'close') end it 'sends email to user2 about assign of new merge_request' do diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index c77e6e9cd50..6f49a65d795 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -64,9 +64,9 @@ describe MergeRequests::Conflicts::ResolveService do end it 'creates a commit with the correct parents' do - expect(merge_request.source_branch_head.parents.map(&:id)). - to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 - 824be604a34828eb682305f0d963056cfac87b2d)) + expect(merge_request.source_branch_head.parents.map(&:id)) + .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 + 824be604a34828eb682305f0d963056cfac87b2d)) end end @@ -129,9 +129,8 @@ describe MergeRequests::Conflicts::ResolveService do it 'creates a commit with the correct parents' do resolve_conflicts - expect(merge_request_from_fork.source_branch_head.parents.map(&:id)). - to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', - target_head]) + expect(merge_request_from_fork.source_branch_head.parents.map(&:id)) + .to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head]) end end end @@ -169,9 +168,9 @@ describe MergeRequests::Conflicts::ResolveService do end it 'creates a commit with the correct parents' do - expect(merge_request.source_branch_head.parents.map(&:id)). - to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 - 824be604a34828eb682305f0d963056cfac87b2d)) + expect(merge_request.source_branch_head.parents.map(&:id)) + .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 + 824be604a34828eb682305f0d963056cfac87b2d)) end it 'sets the content to the content given' do @@ -204,8 +203,8 @@ describe MergeRequests::Conflicts::ResolveService do end it 'raises a MissingResolution error' do - expect { service.execute(user, invalid_params) }. - to raise_error(Gitlab::Conflict::File::MissingResolution) + expect { service.execute(user, invalid_params) } + .to raise_error(Gitlab::Conflict::File::MissingResolution) end end @@ -230,8 +229,8 @@ describe MergeRequests::Conflicts::ResolveService do end it 'raises a MissingResolution error' do - expect { service.execute(user, invalid_params) }. - to raise_error(Gitlab::Conflict::File::MissingResolution) + expect { service.execute(user, invalid_params) } + .to raise_error(Gitlab::Conflict::File::MissingResolution) end end @@ -250,8 +249,8 @@ describe MergeRequests::Conflicts::ResolveService do end it 'raises a MissingFiles error' do - expect { service.execute(user, invalid_params) }. - to raise_error(described_class::MissingFiles) + expect { service.execute(user, invalid_params) } + .to raise_error(described_class::MissingFiles) end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 2963f62cc7d..36a2b672473 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -83,9 +83,9 @@ describe MergeRequests::CreateService, services: true do let!(:pipeline_3) { create(:ci_pipeline, project: project, ref: "other_branch", project_id: project.id) } before do - project.merge_requests. - where(source_branch: opts[:source_branch], target_branch: opts[:target_branch]). - destroy_all + project.merge_requests + .where(source_branch: opts[:source_branch], target_branch: opts[:target_branch]) + .destroy_all end it 'sets head pipeline' do @@ -108,7 +108,7 @@ describe MergeRequests::CreateService, services: true do end end - it_behaves_like 'new issuable record that supports slash commands' do + it_behaves_like 'new issuable record that supports quick actions' do let(:default_params) do { source_branch: 'feature', @@ -117,7 +117,7 @@ describe MergeRequests::CreateService, services: true do end end - context 'Slash commands' do + context 'Quick actions' do context 'with assignee and milestone in params and command' do let(:merge_request) { described_class.new(project, user, opts).execute } let(:milestone) { create(:milestone, project: project) } @@ -150,7 +150,9 @@ describe MergeRequests::CreateService, services: true do context 'asssignee_id' do let(:assignee) { create(:user) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'removes assignee_id when user id is invalid' do opts = { title: 'Title', description: 'Description', assignee_id: -1 } diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index d96f819e66a..711059208c1 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -81,7 +81,9 @@ describe MergeRequests::MergeService, services: true do end context "when jira_issue_transition_id is not present" do - before { allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) } + before do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) + end it "does not close issue" do allow(jira_tracker).to receive_messages(jira_issue_transition_id: nil) @@ -139,9 +141,9 @@ describe MergeRequests::MergeService, services: true do end it 'removes the source branch' do - expect(DeleteBranchService).to receive(:new). - with(merge_request.source_project, merge_request.author). - and_call_original + expect(DeleteBranchService).to receive(:new) + .with(merge_request.source_project, merge_request.author) + .and_call_original service.execute(merge_request) end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 1f109eab268..671a932441e 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -57,8 +57,8 @@ describe MergeRequests::RefreshService, services: true do end it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update', @oldrev) + expect(refresh_service).to have_received(:execute_hooks) + .with(@merge_request, 'update', @oldrev) expect(@merge_request.notes).not_to be_empty expect(@merge_request).to be_open @@ -83,8 +83,8 @@ describe MergeRequests::RefreshService, services: true do end it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update', @oldrev) + expect(refresh_service).to have_received(:execute_hooks) + .with(@merge_request, 'update', @oldrev) expect(@merge_request.notes).not_to be_empty expect(@merge_request).to be_open @@ -146,8 +146,8 @@ describe MergeRequests::RefreshService, services: true do end it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update', @oldrev) + expect(refresh_service).to have_received(:execute_hooks) + .with(@fork_merge_request, 'update', @oldrev) expect(@merge_request.notes).to be_empty expect(@merge_request).to be_open @@ -228,8 +228,8 @@ describe MergeRequests::RefreshService, services: true do let(:refresh_service) { service.new(@fork_project, @user) } it 'refreshes the merge request' do - expect(refresh_service).to receive(:execute_hooks). - with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) + expect(refresh_service).to receive(:execute_hooks) + .with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev) refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index b6d4db2f922..6cc403bdb7f 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -31,8 +31,8 @@ describe MergeRequests::ReopenService, services: true do it { expect(merge_request).to be_reopened } it 'executes hooks with reopen action' do - expect(service).to have_received(:execute_hooks). - with(merge_request, 'reopen') + expect(service).to have_received(:execute_hooks) + .with(merge_request, 'reopen') end it 'sends email to user2 about reopen of merge_request' do diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 091c193aaa6..ec15b5cac14 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -78,8 +78,8 @@ describe MergeRequests::UpdateService, services: true do end it 'executes hooks with update action' do - expect(service).to have_received(:execute_hooks). - with(@merge_request, 'update') + expect(service).to have_received(:execute_hooks) + .with(@merge_request, 'update') end it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do @@ -195,8 +195,8 @@ describe MergeRequests::UpdateService, services: true do head_pipeline_of: merge_request ) - expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user). - and_return(service_mock) + expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user) + .and_return(service_mock) expect(service_mock).to receive(:execute).with(merge_request) end @@ -356,7 +356,9 @@ describe MergeRequests::UpdateService, services: true do end context 'when issue has the `label` label' do - before { merge_request.labels << label } + before do + merge_request.labels << label + end it 'does not send notifications for existing labels' do opts = { label_ids: [label.id, label2.id] } @@ -388,12 +390,16 @@ describe MergeRequests::UpdateService, services: true do end context 'when MergeRequest has tasks' do - before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } + before do + update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) + end it { expect(@merge_request.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) } + before do + update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) + end it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index c9954dc3603..9a98499826f 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -1,15 +1,17 @@ require 'spec_helper' -describe Notes::SlashCommandsService, services: true do +describe Notes::QuickActionsService, services: true do shared_context 'note on noteable' do let(:project) { create(:empty_project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:assignee) { create(:user) } - before { project.team << [assignee, :master] } + before do + project.team << [assignee, :master] + end end - shared_examples 'note on noteable that does not support slash commands' do + shared_examples 'note on noteable that does not support quick actions' do include_context 'note on noteable' before do @@ -43,7 +45,7 @@ describe Notes::SlashCommandsService, services: true do end end - shared_examples 'note on noteable that supports slash commands' do + shared_examples 'note on noteable that supports quick actions' do include_context 'note on noteable' before do @@ -208,15 +210,15 @@ describe Notes::SlashCommandsService, services: true do describe '#execute' do let(:service) { described_class.new(project, master) } - it_behaves_like 'note on noteable that supports slash commands' do + it_behaves_like 'note on noteable that supports quick actions' do let(:note) { build(:note_on_issue, project: project) } end - it_behaves_like 'note on noteable that supports slash commands' do + it_behaves_like 'note on noteable that supports quick actions' do let(:note) { build(:note_on_merge_request, project: project) } end - it_behaves_like 'note on noteable that does not support slash commands' do + it_behaves_like 'note on noteable that does not support quick actions' do let(:note) { build(:note_on_commit, project: project) } end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index de3bbc6b6a1..f1e00c1163b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1539,8 +1539,7 @@ describe NotificationService, services: true do # When resource is nil it means global notification def update_custom_notification(event, user, resource: nil, value: true) setting = user.notification_settings_for(resource) - setting.events[event] = value - setting.save + setting.update!(event => value) end def add_users_with_subscription(project, issuable) diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb index aa63fe3a5c1..cf38c7c75e5 100644 --- a/spec/services/pages_service_spec.rb +++ b/spec/services/pages_service_spec.rb @@ -10,10 +10,14 @@ describe PagesService, services: true do end context 'execute asynchronously for pages job' do - before { build.name = 'pages' } + before do + build.name = 'pages' + end context 'on success' do - before { build.success } + before do + build.success + end it 'executes worker' do expect(PagesWorker).to receive(:perform_async) @@ -23,7 +27,9 @@ describe PagesService, services: true do %w(pending running failed canceled).each do |status| context "on #{status}" do - before { build.status = status } + before do + build.status = status + end it 'does not execute worker' do expect(PagesWorker).not_to receive(:perform_async) diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index b2fb5c91313..4fd9cb23ae1 100644 --- a/spec/services/preview_markdown_service_spec.rb +++ b/spec/services/preview_markdown_service_spec.rb @@ -19,24 +19,24 @@ describe PreviewMarkdownService do end end - context 'new note with slash commands' do + context 'new note with quick actions' do let(:issue) { create(:issue, project: project) } let(:params) do { text: "Please do it\n/assign #{user.to_reference}", - slash_commands_target_type: 'Issue', - slash_commands_target_id: issue.id + quick_actions_target_type: 'Issue', + quick_actions_target_id: issue.id } end let(:service) { described_class.new(project, user, params) } - it 'removes slash commands from text' do + it 'removes quick actions from text' do result = service.execute expect(result[:text]).to eq 'Please do it' end - it 'explains slash commands effect' do + it 'explains quick actions effect' do result = service.execute expect(result[:commands]).to eq "Assigns #{user.to_reference}." @@ -47,21 +47,21 @@ describe PreviewMarkdownService do let(:params) do { text: "My work\n/estimate 2y", - slash_commands_target_type: 'MergeRequest' + quick_actions_target_type: 'MergeRequest' } end let(:service) { described_class.new(project, user, params) } - it 'removes slash commands from text' do + it 'removes quick actions from text' do result = service.execute expect(result[:text]).to eq 'My work' end - it 'explains slash commands effect' do + it 'explains quick actions effect' do result = service.execute expect(result[:commands]).to eq 'Sets time estimate to 2y.' - end + end end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 0d6dd28e332..697dc18feb0 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -15,8 +15,9 @@ describe Projects::DestroyService, services: true do shared_examples 'deleting the project' do it 'deletes the project' do expect(Project.unscoped.all).not_to include(project) - expect(Dir.exist?(path)).to be_falsey - expect(Dir.exist?(remove_path)).to be_falsey + + expect(project.gitlab_shell.exists?(project.repository_storage_path, path + '.git')).to be_falsey + expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path + '.git')).to be_falsey end end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index fff12beed71..ebed802708d 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -66,14 +66,14 @@ describe Projects::HousekeepingService do allow(subject).to receive(:lease_key).and_return(:the_lease_key) # At push 200 - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid). - exactly(1).times + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid) + .exactly(1).times # At push 50, 100, 150 - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid). - exactly(3).times + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid) + .exactly(3).times # At push 10, 20, ... (except those above) - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid). - exactly(16).times + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid) + .exactly(16).times 201.times do subject.increment! diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 44db299812f..e855de38037 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -111,11 +111,11 @@ describe Projects::ImportService, services: true do end it 'flushes various caches' do - allow_any_instance_of(Repository).to receive(:fetch_remote). - and_return(true) + allow_any_instance_of(Repository).to receive(:fetch_remote) + .and_return(true) - allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute). - and_return(true) + allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute) + .and_return(true) expect_any_instance_of(Repository).to receive(:expire_content_cache) diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb index 8a6a9f09f74..a6d43c4f0f1 100644 --- a/spec/services/projects/propagate_service_template_spec.rb +++ b/spec/services/projects/propagate_service_template_spec.rb @@ -60,8 +60,8 @@ describe Projects::PropagateServiceTemplate, services: true do Service.build_from_template(project.id, service_template).save! Service.build_from_template(project.id, other_service).save! - expect { described_class.propagate(service_template) }. - not_to change { Service.count } + expect { described_class.propagate(service_template) } + .not_to change { Service.count } end it 'creates the service containing the template attributes' do @@ -90,8 +90,8 @@ describe Projects::PropagateServiceTemplate, services: true do it 'updates the project external tracker' do service_template.update!(category: 'issue_tracker', default: false) - expect { described_class.propagate(service_template) }. - to change { project.reload.has_external_issue_tracker }.to(true) + expect { described_class.propagate(service_template) } + .to change { project.reload.has_external_issue_tracker }.to(true) end end @@ -99,8 +99,8 @@ describe Projects::PropagateServiceTemplate, services: true do it 'updates the project external tracker' do service_template.update!(type: 'ExternalWikiService') - expect { described_class.propagate(service_template) }. - to change { project.reload.has_external_wiki }.to(true) + expect { described_class.propagate(service_template) } + .to change { project.reload.has_external_wiki }.to(true) end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index b957517c715..76c52d55ae5 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -7,10 +7,10 @@ describe Projects::TransferService, services: true do context 'namespace -> namespace' do before do - allow_any_instance_of(Gitlab::UploadsTransfer). - to receive(:move_project).and_return(true) - allow_any_instance_of(Gitlab::PagesTransfer). - to receive(:move_project).and_return(true) + allow_any_instance_of(Gitlab::UploadsTransfer) + .to receive(:move_project).and_return(true) + allow_any_instance_of(Gitlab::PagesTransfer) + .to receive(:move_project).and_return(true) group.add_owner(user) @result = transfer_project(project, user, group) end @@ -19,6 +19,67 @@ describe Projects::TransferService, services: true do it { expect(project.namespace).to eq(group) } end + context 'when transfer succeeds' do + before do + group.add_owner(user) + end + + it 'sends notifications' do + expect_any_instance_of(NotificationService).to receive(:project_was_moved) + + transfer_project(project, user, group) + end + + it 'executes system hooks' do + expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks) + + transfer_project(project, user, group) + end + end + + context 'when transfer fails' do + let!(:original_path) { project_path(project) } + + def attempt_project_transfer + expect do + transfer_project(project, user, group) + end.to raise_error(ActiveRecord::ActiveRecordError) + end + + before do + group.add_owner(user) + + expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR") + end + + def project_path(project) + File.join(project.repository_storage_path, "#{project.path_with_namespace}.git") + end + + def current_path + project_path(project) + end + + it 'rolls back repo location' do + attempt_project_transfer + + expect(Dir.exist?(original_path)).to be_truthy + expect(original_path).to eq current_path + end + + it "doesn't send move notifications" do + expect_any_instance_of(NotificationService).not_to receive(:project_was_moved) + + attempt_project_transfer + end + + it "doesn't run system hooks" do + expect_any_instance_of(Projects::TransferService).not_to receive(:execute_system_hooks) + + attempt_project_transfer + end + end + context 'namespace -> no namespace' do before do @result = transfer_project(project, user, nil) @@ -59,12 +120,16 @@ describe Projects::TransferService, services: true do context 'visibility level' do let(:internal_group) { create(:group, :internal) } - before { internal_group.add_owner(user) } + before do + internal_group.add_owner(user) + end context 'when namespace visibility level < project visibility level' do let(:public_project) { create(:project, :public, :repository, namespace: user.namespace) } - before { transfer_project(public_project, user, internal_group) } + before do + transfer_project(public_project, user, internal_group) + end it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) } end @@ -72,7 +137,9 @@ describe Projects::TransferService, services: true do context 'when namespace visibility level > project visibility level' do let(:private_project) { create(:project, :private, :repository, namespace: user.namespace) } - before { transfer_project(private_project, user, internal_group) } + before do + transfer_project(private_project, user, internal_group) + end it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } end @@ -106,9 +173,9 @@ describe Projects::TransferService, services: true do end it 'only schedules a single job for every user' do - expect(UserProjectAccessChangedService).to receive(:new). - with([owner.id, group_member.id]). - and_call_original + expect(UserProjectAccessChangedService).to receive(:new) + .with([owner.id, group_member.id]) + .and_call_original transfer_project(project, owner, group) end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 23f5555d3e0..d34652bd7ac 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -12,9 +12,9 @@ describe Projects::UnlinkForkService, services: true do let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } before do - allow(MergeRequests::CloseService).to receive(:new). - with(fork_project, user). - and_return(mr_close_service) + allow(MergeRequests::CloseService).to receive(:new) + .with(fork_project, user) + .and_return(mr_close_service) end it 'close all pending merge requests' do diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 4db491fd5f3..c9e63efbc14 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlashCommands::InterpretService, services: true do +describe QuickActions::InterpretService, services: true do let(:project) { create(:empty_project, :public) } let(:developer) { create(:user) } let(:developer2) { create(:user) } @@ -378,7 +378,9 @@ describe SlashCommands::InterpretService, services: true do context 'assign command with multiple assignees' do let(:content) { "/assign @#{developer.username} @#{developer2.username}" } - before{ project.team << [developer2, :developer] } + before do + project.team << [developer2, :developer] + end context 'Issue' do it 'fetches assignee and populates assignee_id if content contains /assign' do @@ -798,7 +800,11 @@ describe SlashCommands::InterpretService, services: true do context 'if the project has multiple boards' do let(:issuable) { issue } - before { create(:board, project: project) } + + before do + create(:board, project: project) + end + it_behaves_like 'empty command' end diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb index 74cba8c014b..5e6e43b7a90 100644 --- a/spec/services/spam_service_spec.rb +++ b/spec/services/spam_service_spec.rb @@ -70,7 +70,9 @@ describe SpamService, services: true do end context 'when not indicated as spam by akismet' do - before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + before do + allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) + end it 'returns false' do expect(check_spam(issue, request, false)).to be_falsey diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb index 63a1e78f274..817fa4262d5 100644 --- a/spec/services/submit_usage_ping_service_spec.rb +++ b/spec/services/submit_usage_ping_service_spec.rb @@ -92,8 +92,8 @@ describe SubmitUsagePingService do end def stub_response(body) - stub_request(:post, 'https://version.gitlab.com/usage_data'). - to_return( + stub_request(:post, 'https://version.gitlab.com/usage_data') + .to_return( headers: { 'Content-Type' => 'application/json' }, body: body.to_json ) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9295c09aefc..8d3dafafab2 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -333,8 +333,8 @@ describe SystemNoteService, services: true do end it 'sets the note text' do - expect(subject.note). - to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**" + expect(subject.note) + .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**" end end end @@ -521,8 +521,8 @@ describe SystemNoteService, services: true do context 'when mentioner is not a MergeRequest' do it 'is falsey' do mentioner = noteable.dup - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_falsey + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_falsey end end @@ -533,14 +533,14 @@ describe SystemNoteService, services: true do it 'is truthy when noteable is in commits' do expect(mentioner).to receive(:commits).and_return([noteable]) - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_truthy + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_truthy end it 'is falsey when noteable is not in commits' do expect(mentioner).to receive(:commits).and_return([]) - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_falsey + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_falsey end end @@ -548,8 +548,8 @@ describe SystemNoteService, services: true do let(:noteable) { ExternalIssue.new('EXT-1234', project) } it 'is truthy' do mentioner = noteable.dup - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_truthy + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_truthy end end end @@ -566,13 +566,13 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(noteable, commit0)). - to be_truthy + expect(described_class.cross_reference_exists?(noteable, commit0)) + .to be_truthy end it 'is falsey when not already mentioned' do - expect(described_class.cross_reference_exists?(noteable, commit1)). - to be_falsey + expect(described_class.cross_reference_exists?(noteable, commit1)) + .to be_falsey end context 'legacy capitalized cross reference' do @@ -583,8 +583,8 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(noteable, commit0)). - to be_truthy + expect(described_class.cross_reference_exists?(noteable, commit0)) + .to be_truthy end end end @@ -596,13 +596,13 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(commit0, commit1)). - to be_truthy + expect(described_class.cross_reference_exists?(commit0, commit1)) + .to be_truthy end it 'is falsey when not already mentioned' do - expect(described_class.cross_reference_exists?(commit1, commit0)). - to be_falsey + expect(described_class.cross_reference_exists?(commit1, commit0)) + .to be_falsey end context 'legacy capitalized cross reference' do @@ -613,8 +613,8 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(commit0, commit1)). - to be_truthy + expect(described_class.cross_reference_exists?(commit0, commit1)) + .to be_truthy end end end @@ -629,8 +629,8 @@ describe SystemNoteService, services: true do end it 'is true when a fork mentions an external issue' do - expect(described_class.cross_reference_exists?(noteable, commit2)). - to be true + expect(described_class.cross_reference_exists?(noteable, commit2)) + .to be true end context 'legacy capitalized cross reference' do @@ -640,8 +640,8 @@ describe SystemNoteService, services: true do end it 'is true when a fork mentions an external issue' do - expect(described_class.cross_reference_exists?(noteable, commit2)). - to be true + expect(described_class.cross_reference_exists?(noteable, commit2)) + .to be true end end end diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb index b9121b1de49..9f143cc5667 100644 --- a/spec/services/tags/create_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -26,9 +26,9 @@ describe Tags::CreateService, services: true do context 'when tag already exists' do it 'returns an error' do - expect(repository).to receive(:add_tag). - with(user, 'v1.1.0', 'master', 'Foo'). - and_raise(Rugged::TagError) + expect(repository).to receive(:add_tag) + .with(user, 'v1.1.0', 'master', 'Foo') + .and_raise(Rugged::TagError) response = service.execute('v1.1.0', 'master', 'Foo') @@ -39,9 +39,9 @@ describe Tags::CreateService, services: true do context 'when pre-receive hook fails' do it 'returns an error' do - expect(repository).to receive(:add_tag). - with(user, 'v1.1.0', 'master', 'Foo'). - and_raise(GitHooksService::PreReceiveError, 'something went wrong') + expect(repository).to receive(:add_tag) + .with(user, 'v1.1.0', 'master', 'Foo') + .and_raise(GitHooksService::PreReceiveError, 'something went wrong') response = service.execute('v1.1.0', 'master', 'Foo') diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb index b4efe7de431..14a5e40350a 100644 --- a/spec/services/user_project_access_changed_service_spec.rb +++ b/spec/services/user_project_access_changed_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe UserProjectAccessChangedService do describe '#execute' do it 'schedules the user IDs' do - expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait). - with([[1], [2]]) + expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait) + .with([[1], [2]]) described_class.new([1, 2]).execute end diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index 8d67ebe3231..2e009d4ce1c 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -41,8 +41,8 @@ describe Users::ActivityService, services: true do end def last_hour_user_ids - Gitlab::UserActivities.new. - select { |k, v| v >= 1.hour.ago.to_i.to_s }. - map { |k, _| k.to_i } + Gitlab::UserActivities.new + .select { |k, v| v >= 1.hour.ago.to_i.to_s } + .map { |k, _| k.to_i } end end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 8c40d25e00c..b65cadbb2f5 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -10,11 +10,11 @@ describe Users::RefreshAuthorizedProjectsService do describe '#execute', :redis do it 'refreshes the authorizations using a lease' do - expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). - and_return('foo') + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) + .and_return('foo') - expect(Gitlab::ExclusiveLease).to receive(:cancel). - with(an_instance_of(String), 'foo') + expect(Gitlab::ExclusiveLease).to receive(:cancel) + .with(an_instance_of(String), 'foo') expect(service).to receive(:execute_without_lease) @@ -29,11 +29,11 @@ describe Users::RefreshAuthorizedProjectsService do it 'updates the authorized projects of the user' do project2 = create(:empty_project) - to_remove = user.project_authorizations. - create!(project: project2, access_level: Gitlab::Access::MASTER) + to_remove = user.project_authorizations + .create!(project: project2, access_level: Gitlab::Access::MASTER) - expect(service).to receive(:update_authorizations). - with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) + expect(service).to receive(:update_authorizations) + .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) service.execute_without_lease end @@ -41,11 +41,11 @@ describe Users::RefreshAuthorizedProjectsService do it 'sets the access level of a project to the highest available level' do user.project_authorizations.delete_all - to_remove = user.project_authorizations. - create!(project: project, access_level: Gitlab::Access::DEVELOPER) + to_remove = user.project_authorizations + .create!(project: project, access_level: Gitlab::Access::DEVELOPER) - expect(service).to receive(:update_authorizations). - with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) + expect(service).to receive(:update_authorizations) + .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) service.execute_without_lease end diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb new file mode 100644 index 00000000000..0b2f840c462 --- /dev/null +++ b/spec/services/users/update_service_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Users::UpdateService, services: true do + let(:user) { create(:user) } + + describe '#execute' do + it 'updates the name' do + result = update_user(user, name: 'New Name') + + expect(result).to eq(status: :success) + expect(user.name).to eq('New Name') + end + + it 'returns an error result when record cannot be updated' do + expect do + update_user(user, { email: 'invalid' }) + end.not_to change { user.reload.email } + end + + def update_user(user, opts) + described_class.new(user, opts).execute + end + end + + describe '#execute!' do + it 'updates the name' do + result = update_user(user, name: 'New Name') + + expect(result).to be true + expect(user.name).to eq('New Name') + end + + it 'raises an error when record cannot be updated' do + expect do + update_user(user, email: 'invalid') + end.to raise_error(ActiveRecord::RecordInvalid) + end + + def update_user(user, opts) + described_class.new(user, opts).execute! + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a81d3573f8d..fdef6fd5221 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,6 +44,7 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view + config.include Devise::Test::IntegrationHelpers, type: :feature config.include Warden::Test::Helpers, type: :request config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature @@ -56,6 +57,7 @@ RSpec.configure do |config| config.include StubGitlabCalls config.include StubGitlabData config.include ApiHelpers, :api + config.include Rails.application.routes.url_helpers, type: :routing config.include MigrationsHelpers, :migration config.infer_spec_type_from_file_location! diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb index e42d727672b..dff0dfba675 100644 --- a/spec/support/api/schema_matcher.rb +++ b/spec/support/api/schema_matcher.rb @@ -1,8 +1,16 @@ +def schema_path(schema) + schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas" + "#{schema_directory}/#{schema}.json" +end + RSpec::Matchers.define :match_response_schema do |schema, **options| match do |response| - schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas" - schema_path = "#{schema_directory}/#{schema}.json" + JSON::Validator.validate!(schema_path(schema), response.body, options) + end +end - JSON::Validator.validate!(schema_path, response.body, options) +RSpec::Matchers.define :match_schema do |schema, **options| + match do |data| + JSON::Validator.validate!(schema_path(schema), data, options) end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index c34e76fa72f..3e5d6cf1364 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -35,4 +35,14 @@ RSpec.configure do |config| TestEnv.eager_load_driver_server $capybara_server_already_started = true end + + config.after(:each, :js) do |example| + # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook, + # but `block_and_wait_for_requests_complete` is called before it so by + # calling it explicitely here, we prevent any new requests from being fired + # See https://github.com/teamcapybara/capybara/blob/ffb41cfad620de1961bb49b1562a9fa9b28c0903/lib/capybara/rspec.rb#L20-L25 + # We don't reset the session when the example failed, because we need capybara-screenshot to have access to it. + Capybara.reset_sessions! unless example.exception + block_and_wait_for_requests_complete + end end diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/chat_slash_commands_shared_examples.rb index 4dfa29849ee..978b0b9cc30 100644 --- a/spec/support/chat_slash_commands_shared_examples.rb +++ b/spec/support/chat_slash_commands_shared_examples.rb @@ -87,7 +87,7 @@ RSpec.shared_examples 'chat slash commands service' do end it 'triggers the command' do - expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) + expect_any_instance_of(Gitlab::SlashCommands::Command).to receive(:execute) subject.trigger(params) end diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index d6b40db09ce..a8d9566b4e4 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -14,8 +14,8 @@ shared_examples 'a GitHub-ish import controller: POST personal_access_token' do it "updates access token" do token = 'asdfasdf9876' - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:user).and_return(true) + allow_any_instance_of(Gitlab::GithubImport::Client) + .to receive(:user).and_return(true) post :personal_access_token, personal_access_token: token @@ -79,8 +79,8 @@ shared_examples 'a GitHub-ish import controller: GET status' do end it "handles an invalid access token" do - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:repos).and_raise(Octokit::Unauthorized) + allow_any_instance_of(Gitlab::GithubImport::Client) + .to receive(:repos).and_raise(Octokit::Unauthorized) get :status @@ -110,9 +110,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do context "when the repository owner is the provider user" do context "when the provider user and GitLab user's usernames match" do it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, format: :js end @@ -122,9 +122,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do let(:provider_username) { "someone_else" } it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, format: :js end @@ -144,9 +144,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do context "when the namespace is owned by the GitLab user" do it "takes the existing namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, format: :js end @@ -159,9 +159,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do end it "creates a project using user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, format: :js end @@ -171,16 +171,16 @@ shared_examples 'a GitHub-ish import controller: POST create' do context "when a namespace with the provider user's username doesn't exist" do context "when current user can create namespaces" do it "creates the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).and_return(double(execute: true)) expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1) end it "takes the new namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider) + .and_return(double(execute: true)) post :create, target_namespace: provider_repo.name, format: :js end @@ -192,16 +192,16 @@ shared_examples 'a GitHub-ish import controller: POST create' do end it "doesn't create the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).and_return(double(execute: true)) expect { post :create, format: :js }.not_to change(Namespace, :count) end it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, format: :js end @@ -217,17 +217,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do end it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } end it 'takes the selected name and default namespace' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, { new_name: test_name, format: :js } end @@ -243,9 +243,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do end it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider) + .and_return(double(execute: true)) post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js } end @@ -255,26 +255,26 @@ shared_examples 'a GitHub-ish import controller: POST create' do let(:test_name) { 'test_name' } it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } end it 'creates the namespaces' do - allow(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider). - and_return(double(execute: true)) + allow(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: true)) expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } } .to change { Namespace.count }.by(2) end it 'new namespace has the right parent' do - allow(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider). - and_return(double(execute: true)) + allow(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } @@ -287,17 +287,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider). - and_return(double(execute: true)) + expect(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: true)) post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } end it 'creates the namespaces' do - allow(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider). - and_return(double(execute: true)) + allow(Gitlab::GithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: true)) expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } } .to change { Namespace.count }.by(2) diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index fa82dc5e9f9..50869099bb7 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -1,8 +1,8 @@ # Specifications for behavior common to all objects with executable attributes. # It takes a `issuable_type`, and expect an `issuable`. -shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type| - include SlashCommandsHelpers +shared_examples 'issuable record that supports quick actions in its description and notes' do |issuable_type| + include QuickActionsHelpers let(:master) { create(:user) } let(:assignee) { create(:user, username: 'bob') } @@ -17,7 +17,7 @@ shared_examples 'issuable record that supports slash commands in its description project.team << [master, :master] project.team << [assignee, :developer] project.team << [guest, :guest] - login_with(master) + gitlab_sign_in(master) end after do @@ -105,8 +105,8 @@ shared_examples 'issuable record that supports slash commands in its description context "when current user cannot close #{issuable_type}" do before do - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -140,8 +140,8 @@ shared_examples 'issuable record that supports slash commands in its description context "when current user cannot reopen #{issuable_type}" do before do - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -170,8 +170,8 @@ shared_examples 'issuable record that supports slash commands in its description context "when current user cannot change title of #{issuable_type}" do before do - logout - login_with(guest) + gitlab_sign_out + gitlab_sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -260,7 +260,7 @@ shared_examples 'issuable record that supports slash commands in its description end describe "preview of note on #{issuable_type}" do - it 'removes slash commands from note and explains them' do + it 'removes quick actions from note and explains them' do visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) page.within('.js-main-target-form') do diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 0d80c95e826..27e079c01dd 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -13,9 +13,7 @@ shared_examples 'reportable note' do it 'dropdown has Edit, Report and Delete links' do dropdown = comment.find(more_actions_selector) - - dropdown.click - dropdown.find('.dropdown-menu li', match: :first) + open_dropdown(dropdown) expect(dropdown).to have_button('Edit comment') expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) @@ -24,13 +22,16 @@ shared_examples 'reportable note' do it 'Report button links to a report page' do dropdown = comment.find(more_actions_selector) - - dropdown.click - dropdown.find('.dropdown-menu li', match: :first) + open_dropdown(dropdown) dropdown.click_link('Report as abuse') expect(find('#user_name')['value']).to match(note.author.username) expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note)) end + + def open_dropdown(dropdown) + dropdown.click + dropdown.find('.dropdown-menu li', match: :first) + end end diff --git a/spec/support/filter_item_select_helper.rb b/spec/support/filter_item_select_helper.rb new file mode 100644 index 00000000000..519e84d359e --- /dev/null +++ b/spec/support/filter_item_select_helper.rb @@ -0,0 +1,19 @@ +# Helper allows you to select value from filter-items +# +# Params +# value - value for select +# selector - css selector of item +# +# Usage: +# +# filter_item_select('Any Author', '.js-author-search') +# +module FilterItemSelectHelper + def filter_item_select(value, selector) + find(selector).click + wait_for_requests + page.within('.dropdown-content') do + click_link value + end + end +end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index 37cc308e613..d21c4324d9e 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -14,6 +14,9 @@ module FilteredSearchHelpers filtered_search.set(search) if submit + # Wait for the lazy author/assignee tokens that + # swap out the username with an avatar and name + wait_for_requests filtered_search.send_keys(:enter) end end diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index 7335f74c0e9..c89389b90ca 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -15,7 +15,7 @@ require 'erb' require 'tempfile' -SOURCE = 'https://gitlab.com/gitlab-org/gitlab-git-test.git'.freeze +SOURCE = File.expand_path('../gitlab-git-test.git', __FILE__).freeze SCRIPT_NAME = 'generate-seed-repo-rb'.freeze REPO_NAME = 'gitlab-git-test.git'.freeze diff --git a/spec/support/gitlab-git-test.git/HEAD b/spec/support/gitlab-git-test.git/HEAD new file mode 100644 index 00000000000..cb089cd89a7 --- /dev/null +++ b/spec/support/gitlab-git-test.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/spec/support/gitlab-git-test.git/README.md b/spec/support/gitlab-git-test.git/README.md new file mode 100644 index 00000000000..f072cd421be --- /dev/null +++ b/spec/support/gitlab-git-test.git/README.md @@ -0,0 +1,16 @@ +# Gitlab::Git test repository + +This repository is used by (some of) the tests in spec/lib/gitlab/git. + +Do not add new large files to this repository. Otherwise we needlessly +inflate the size of the gitlab-ce repository. + +## How to make changes to this repository + +- (if needed) clone `https://gitlab.com/gitlab-org/gitlab-ce.git` to your local machine +- clone `gitlab-ce/spec/support/gitlab-git-test.git` locally (i.e. clone from your hard drive, not from the internet) +- make changes in your local clone of gitlab-git-test +- run `git push` which will push to your local source `gitlab-ce/spec/support/gitlab-git-test.git` +- in gitlab-ce: run `spec/support/prepare-gitlab-git-test-for-commit` +- in gitlab-ce: `git add spec/support/seed_repo.rb spec/support/gitlab-git-test.git` +- commit your changes in gitlab-ce diff --git a/spec/support/gitlab-git-test.git/config b/spec/support/gitlab-git-test.git/config new file mode 100644 index 00000000000..03e2d1b1e0f --- /dev/null +++ b/spec/support/gitlab-git-test.git/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + precomposeunicode = true +[remote "origin"] + url = https://gitlab.com/gitlab-org/gitlab-git-test.git diff --git a/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.idx b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.idx Binary files differnew file mode 100644 index 00000000000..2253da798c4 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.idx diff --git a/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.pack b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.pack Binary files differnew file mode 100644 index 00000000000..3a61107c5b1 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.pack diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs new file mode 100644 index 00000000000..ce5ab1f705b --- /dev/null +++ b/spec/support/gitlab-git-test.git/packed-refs @@ -0,0 +1,18 @@ +# pack-refs with: peeled fully-peeled +0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature +12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix +6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path +58fa1a3af4de73ea83fe25a1ef1db8e0c56f67e5 refs/heads/fix-existing-submodule-dir +40f4a7a617393735a95a0bb67b08385bc1e7c66d refs/heads/fix-mode +9abd6a8c113a2dd76df3fdb3d58a8cec6db75f8d refs/heads/gitattributes +46e1395e609395de004cacd4b142865ab0e52a29 refs/heads/gitattributes-updated +4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master +5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test +f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0 +^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 +8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0 +^5937ac0a7beb003549fc5fd26fc247adbce4a52e +10d64eed7760f2811ee2d64b44f1f7d3b364f17b refs/tags/v1.2.0 +^eb49186cfa5c4338011f5f590fac11bd66c5c631 +2ac1f24e253e08135507d0830508febaaccf02ee refs/tags/v1.2.1 +^fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 diff --git a/spec/support/gitlab-git-test.git/refs/heads/.gitkeep b/spec/support/gitlab-git-test.git/refs/heads/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/support/gitlab-git-test.git/refs/heads/.gitkeep diff --git a/spec/support/gitlab-git-test.git/refs/tags/.gitkeep b/spec/support/gitlab-git-test.git/refs/tags/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/support/gitlab-git-test.git/refs/tags/.gitkeep diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index 9280fad4ace..c92f78b324c 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -1,7 +1,26 @@ module KubernetesHelpers include Gitlab::Kubernetes - def kube_discovery_body + def kube_response(body) + { body: body.to_json } + end + + def kube_pods_response + kube_response(kube_pods_body) + end + + def stub_kubeclient_discover + WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) + end + + def stub_kubeclient_pods(response = nil) + stub_kubeclient_discover + pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" + + WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) + end + + def kube_v1_discovery_body { "kind" => "APIResourceList", "resources" => [ @@ -10,17 +29,19 @@ module KubernetesHelpers } end - def kube_pods_body(*pods) - { "kind" => "PodList", - "items" => [kube_pod] } + def kube_pods_body + { + "kind" => "PodList", + "items" => [kube_pod] + } end # This is a partial response, it will have many more elements in reality but # these are the ones we care about at the moment - def kube_pod(app: "valid-pod-label") + def kube_pod(name: "kube-pod", app: "valid-pod-label") { "metadata" => { - "name" => "kube-pod", + "name" => name, "creationTimestamp" => "2016-11-25T19:55:19Z", "labels" => { "app" => app } }, diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index e6da852e728..4c88958264b 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -6,15 +6,15 @@ module LoginHelpers # Examples: # # # Create a user automatically - # login_as(:user) + # gitlab_sign_in(:user) # # # Create an admin automatically - # login_as(:admin) + # gitlab_sign_in(:admin) # # # Provide an existing User record # user = create(:user) - # login_as(user) - def login_as(user_or_role) + # gitlab_sign_in(user) + def gitlab_sign_in(user_or_role, **kwargs) @user = if user_or_role.is_a?(User) user_or_role @@ -22,26 +22,44 @@ module LoginHelpers create(user_or_role) end - login_with(@user) + gitlab_sign_in_with(@user, **kwargs) end - # Internal: Login as the specified user + def gitlab_sign_in_via(provider, user, uid) + mock_auth_hash(provider, uid, user.email) + visit new_user_session_path + click_link provider + end + + # Requires Javascript driver. + def gitlab_sign_out + find(".header-user-dropdown-toggle").click + click_link "Sign out" + # check the sign_in button + expect(page).to have_button('Sign in') + end + + # Logout without JavaScript driver + def gitlab_sign_out_direct + page.driver.submit :delete, '/users/sign_out', {} + end + + private + + # Private: Login as the specified user # # user - User instance to login with # remember - Whether or not to check "Remember me" (default: false) - def login_with(user, remember: false) + def gitlab_sign_in_with(user, remember: false) visit new_user_session_path + fill_in "user_login", with: user.email fill_in "user_password", with: "12345678" check 'user_remember_me' if remember + click_button "Sign in" - Thread.current[:current_user] = user - end - def login_via(provider, user, uid) - mock_auth_hash(provider, uid, user.email) - visit new_user_session_path - click_link provider + Thread.current[:current_user] = user end def mock_auth_hash(provider, uid, email) @@ -72,16 +90,24 @@ module LoginHelpers Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml] end - # Requires Javascript driver. - def logout - find(".header-user-dropdown-toggle").click - click_link "Sign out" - # check the sign_in button - expect(page).to have_button('Sign in') + def mock_saml_config + OpenStruct.new(name: 'saml', label: 'saml', args: { + assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback', + idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52', + idp_sso_target_url: 'https://idp.example.com/sso/saml', + issuer: 'https://localhost:3443/', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }) end - # Logout without JavaScript driver - def logout_direct - page.driver.submit :delete, '/users/sign_out', {} + def stub_omniauth_saml_config(messages) + Rails.application.env_config['devise.mapping'] = Devise.mappings[:user] + Rails.application.routes.disable_clear_and_finalize = true + Rails.application.routes.draw do + post '/users/auth/saml' => 'omniauth_callbacks#saml' + end + allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) + stub_omniauth_setting(messages) + expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') end end diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb index ed14bcec9f2..ebfabcd8f24 100644 --- a/spec/support/matchers/gitaly_matchers.rb +++ b/spec/support/matchers/gitaly_matchers.rb @@ -1,5 +1,10 @@ -RSpec::Matchers.define :gitaly_request_with_repo_path do |path| - match { |actual| actual.repository.path == path } +RSpec::Matchers.define :gitaly_request_with_path do |storage_name, relative_path| + match do |actual| + repository = actual.repository + + repository.storage_name == storage_name && + repository.relative_path == relative_path + end end RSpec::Matchers.define :gitaly_request_with_params do |params| diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 87936bb4859..3ac201f1fb1 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -81,8 +81,8 @@ shared_examples 'a mentionable' do ext_issue, ext_mr, ext_commit] mentioned_objects.each do |referenced| - expect(SystemNoteService).to receive(:cross_reference). - with(referenced, subject.local_reference, author) + expect(SystemNoteService).to receive(:cross_reference) + .with(referenced, subject.local_reference, author) end subject.create_cross_references! @@ -127,15 +127,15 @@ shared_examples 'an editable mentionable' do # These three objects were already referenced, and should not receive new # notes [mentioned_issue, mentioned_commit, ext_issue].each do |oldref| - expect(SystemNoteService).not_to receive(:cross_reference). - with(oldref, any_args) + expect(SystemNoteService).not_to receive(:cross_reference) + .with(oldref, any_args) end # These two issues are new and should receive reference notes # In the case of MergeRequests remember that cannot mention commits included in the MergeRequest new_issues.each do |newref| - expect(SystemNoteService).to receive(:cross_reference). - with(newref, subject.local_reference, author) + expect(SystemNoteService).to receive(:cross_reference) + .with(newref, subject.local_reference, author) end set_mentionable_text.call(new_text) diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb index 7cfc1e06975..70b499198bf 100644 --- a/spec/support/milestone_tabs_examples.rb +++ b/spec/support/milestone_tabs_examples.rb @@ -15,7 +15,9 @@ shared_examples 'milestone tabs' do describe '#merge_requests' do context 'as html' do - before { go(:merge_requests, format: 'html') } + before do + go(:merge_requests, format: 'html') + end it 'redirects to milestone#show' do expect(response).to redirect_to(milestone_path) @@ -23,7 +25,9 @@ shared_examples 'milestone tabs' do end context 'as json' do - before { go(:merge_requests, format: 'json') } + before do + go(:merge_requests, format: 'json') + end it 'renders the merge requests tab template to a string' do expect(response).to render_template('shared/milestones/_merge_requests_tab') @@ -34,7 +38,9 @@ shared_examples 'milestone tabs' do describe '#participants' do context 'as html' do - before { go(:participants, format: 'html') } + before do + go(:participants, format: 'html') + end it 'redirects to milestone#show' do expect(response).to redirect_to(milestone_path) @@ -42,7 +48,9 @@ shared_examples 'milestone tabs' do end context 'as json' do - before { go(:participants, format: 'json') } + before do + go(:participants, format: 'json') + end it 'renders the participants tab template to a string' do expect(response).to render_template('shared/milestones/_participants_tab') @@ -53,7 +61,9 @@ shared_examples 'milestone tabs' do describe '#labels' do context 'as html' do - before { go(:labels, format: 'html') } + before do + go(:labels, format: 'html') + end it 'redirects to milestone#show' do expect(response).to redirect_to(milestone_path) @@ -61,7 +71,9 @@ shared_examples 'milestone tabs' do end context 'as json' do - before { go(:labels, format: 'json') } + before do + go(:labels, format: 'json') + end it 'renders the labels tab template to a string' do expect(response).to render_template('shared/milestones/_labels_tab') diff --git a/spec/support/prepare-gitlab-git-test-for-commit b/spec/support/prepare-gitlab-git-test-for-commit new file mode 100755 index 00000000000..3047786a599 --- /dev/null +++ b/spec/support/prepare-gitlab-git-test-for-commit @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +abort unless [ + system('spec/support/generate-seed-repo-rb', out: 'spec/support/seed_repo.rb'), + system('spec/support/unpack-gitlab-git-test') +].all? + +exit if ARGV.first != '--check-for-changes' + +git_status = IO.popen(%w[git status --porcelain], &:read) +abort unless $?.success? + +puts git_status + +if git_status.lines.grep(%r{^.. spec/support/gitlab-git-test.git}).any? + abort "error: detected changes in gitlab-git-test.git" +end diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb index f8b7d0527ba..81b51509e0b 100644 --- a/spec/support/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb @@ -18,7 +18,7 @@ shared_examples 'project features apply to issuables' do |klass| before do _ = issuable - login_as(user) if user + gitlab_sign_in(user) if user visit path end diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb new file mode 100644 index 00000000000..016e16fc8d4 --- /dev/null +++ b/spec/support/prometheus/additional_metrics_shared_examples.rb @@ -0,0 +1,101 @@ +RSpec.shared_examples 'additional metrics query' do + include Prometheus::MetricBuilders + + let(:metric_group_class) { Gitlab::Prometheus::MetricGroup } + let(:metric_class) { Gitlab::Prometheus::Metric } + + let(:metric_names) { %w{metric_a metric_b} } + + let(:query_range_result) do + [{ 'metric': {}, 'values': [[1488758662.506, '0.00002996364761904785'], [1488758722.506, '0.00003090239047619091']] }] + end + + before do + allow(client).to receive(:label_values).and_return(metric_names) + allow(metric_group_class).to receive(:all).and_return([simple_metric_group(metrics: [simple_metric])]) + end + + context 'with one group where two metrics is found' do + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + end + + context 'some queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_empty', any_args).and_return([]) + end + + it 'return group data only for queries with results' do + expected = [ + { + group: 'name', + priority: 1, + metrics: [ + { + title: 'title', weight: 1, y_label: 'Values', queries: [ + { query_range: 'query_range_a', result: query_range_result }, + { query_range: 'query_range_b', label: 'label', unit: 'unit', result: query_range_result } + ] + } + ] + } + ] + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + expect(query_result).to eq(expected) + end + end + end + + context 'with two groups with one metric each' do + let(:metrics) { [simple_metric(queries: [simple_query])] } + before do + allow(metric_group_class).to receive(:all).and_return( + [ + simple_metric_group(name: 'group_a', metrics: [simple_metric(queries: [simple_query])]), + simple_metric_group(name: 'group_b', metrics: [simple_metric(title: 'title_b', queries: [simple_query('b')])]) + ]) + allow(client).to receive(:label_values).and_return(metric_names) + end + + context 'both queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + end + + it 'return group data both queries' do + queries_with_result_a = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + queries_with_result_b = { queries: [{ query_range: 'query_range_b', result: query_range_result }] } + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(2) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result[0][:metrics].first).to include(queries_with_result_a) + expect(query_result[1][:metrics].first).to include(queries_with_result_b) + end + end + + context 'one query returns result' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return([]) + end + + it 'return group data only for query with results' do + queries_with_result = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(1) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result.first[:metrics].first).to include(queries_with_result) + end + end + end +end diff --git a/spec/support/prometheus/metric_builders.rb b/spec/support/prometheus/metric_builders.rb new file mode 100644 index 00000000000..c8d056d3fc8 --- /dev/null +++ b/spec/support/prometheus/metric_builders.rb @@ -0,0 +1,27 @@ +module Prometheus + module MetricBuilders + def simple_query(suffix = 'a', **opts) + { query_range: "query_range_#{suffix}" }.merge(opts) + end + + def simple_queries + [simple_query, simple_query('b', label: 'label', unit: 'unit')] + end + + def simple_metric(title: 'title', required_metrics: [], queries: [simple_query]) + Gitlab::Prometheus::Metric.new(title: title, required_metrics: required_metrics, weight: 1, queries: queries) + end + + def simple_metrics(added_metric_name: 'metric_a') + [ + simple_metric(required_metrics: %W(#{added_metric_name} metric_b), queries: simple_queries), + simple_metric(required_metrics: [added_metric_name], queries: [simple_query('empty')]), + simple_metric(required_metrics: %w{metric_c}) + ] + end + + def simple_metric_group(name: 'name', metrics: simple_metrics) + Gitlab::Prometheus::MetricGroup.new(name: name, priority: 1, metrics: metrics) + end + end +end diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb index 6b9ebcf2bb3..4212be2cc88 100644 --- a/spec/support/prometheus_helpers.rb +++ b/spec/support/prometheus_helpers.rb @@ -36,6 +36,19 @@ module PrometheusHelpers "https://prometheus.example.com/api/v1/query_range?#{query}" end + def prometheus_label_values_url(name) + "https://prometheus.example.com/api/v1/label/#{name}/values" + end + + def prometheus_series_url(*matches, start: 8.hours.ago, stop: Time.now) + query = { + match: matches, + start: start.to_f, + end: stop.to_f + }.to_query + "https://prometheus.example.com/api/v1/series?#{query}" + end + def stub_prometheus_request(url, body: {}, status: 200) WebMock.stub_request(:get, url) .to_return({ @@ -85,6 +98,19 @@ module PrometheusHelpers def prometheus_data(last_update: Time.now.utc) { success: true, + data: { + memory_values: prometheus_values_body('matrix').dig(:data, :result), + memory_current: prometheus_value_body('vector').dig(:data, :result), + cpu_values: prometheus_values_body('matrix').dig(:data, :result), + cpu_current: prometheus_value_body('vector').dig(:data, :result) + }, + last_update: last_update + } + end + + def prometheus_metrics_data(last_update: Time.now.utc) + { + success: true, metrics: { memory_values: prometheus_values_body('matrix').dig(:data, :result), memory_current: prometheus_value_body('vector').dig(:data, :result), @@ -140,4 +166,37 @@ module PrometheusHelpers } } end + + def prometheus_label_values + { + 'status': 'success', + 'data': %w(job_adds job_controller_rate_limiter_use job_depth job_queue_latency job_work_duration_sum up) + } + end + + def prometheus_series(name) + { + 'status': 'success', + 'data': [ + { + '__name__': name, + 'container_name': 'gitlab', + 'environment': 'mattermost', + 'id': '/docker/9953982f95cf5010dfc59d7864564d5f188aaecddeda343699783009f89db667', + 'image': 'gitlab/gitlab-ce:8.15.4-ce.1', + 'instance': 'minikube', + 'job': 'kubernetes-nodes', + 'name': 'k8s_gitlab.e6611886_mattermost-4210310111-77z8r_gitlab_2298ae6b-da24-11e6-baee-8e7f67d0eb3a_43536cb6', + 'namespace': 'gitlab', + 'pod_name': 'mattermost-4210310111-77z8r' + }, + { + '__name__': name, + 'id': '/docker', + 'instance': 'minikube', + 'job': 'kubernetes-nodes' + } + ] + } + end end diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/quick_actions_helpers.rb index 4bfe481115f..d2aaae7518f 100644 --- a/spec/support/slash_commands_helpers.rb +++ b/spec/support/quick_actions_helpers.rb @@ -1,4 +1,4 @@ -module SlashCommandsHelpers +module QuickActionsHelpers def write_note(text) Sidekiq::Testing.fake! do page.within('.js-main-target-form') do diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb index 98eb57f8b54..34124f02133 100644 --- a/spec/support/reactive_caching_helpers.rb +++ b/spec/support/reactive_caching_helpers.rb @@ -35,8 +35,8 @@ module ReactiveCachingHelpers end def expect_reactive_cache_update_queued(subject) - expect(ReactiveCachingWorker). - to receive(:perform_in). - with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id) + expect(ReactiveCachingWorker) + .to receive(:perform_in) + .with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id) end end diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb index 8eb74635a60..bd83cb88058 100644 --- a/spec/support/reference_parser_shared_examples.rb +++ b/spec/support/reference_parser_shared_examples.rb @@ -3,7 +3,9 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features| related_features.map { |feature| (feature + "_access_level").to_sym } end - before { link['data-project'] = project.id.to_s } + before do + link['data-project'] = project.id.to_s + end context "when feature is disabled" do it "does not create reference" do @@ -13,7 +15,9 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features| end context "when feature is enabled only for team members" do - before { set_features_fields_to(ProjectFeature::PRIVATE) } + before do + set_features_fields_to(ProjectFeature::PRIVATE) + end it "does not create reference for non member" do non_member = create(:user) diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 47b5f556e66..8731847592b 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -9,7 +9,7 @@ TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze module SeedHelper - GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze + GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __FILE__).freeze def ensure_seeds if File.exist?(SEED_STORAGE_PATH) diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb index 1dd3663b944..9399745f900 100644 --- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb @@ -1,7 +1,7 @@ # Specifications for behavior common to all objects with executable attributes. # It can take a `default_params`. -shared_examples 'new issuable record that supports slash commands' do +shared_examples 'new issuable record that supports quick actions' do let!(:project) { create(:project, :repository) } let(:user) { create(:user).tap { |u| project.team << [u, :master] } } let(:assignee) { create(:user) } @@ -11,7 +11,9 @@ shared_examples 'new issuable record that supports slash commands' do let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) } let(:issuable) { described_class.new(project, user, params).execute } - before { project.team << [assignee, :master] } + before do + project.team << [assignee, :master] + end context 'with labels in command only' do let(:example_params) do diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb index 8947f20562f..ffbce6c42bf 100644 --- a/spec/support/services/issuable_update_service_shared_examples.rb +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -4,7 +4,9 @@ shared_examples 'issuable update service' do end context 'changing state' do - before { expect(project).to receive(:execute_hooks).once } + before do + expect(project).to receive(:execute_hooks).once + end context 'to reopened' do it 'executes hooks only once' do diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index 66c93890e31..7457484a932 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -6,9 +6,9 @@ Service.available_services_names.each do |service| let(:service_fields) { service_klass.new.fields } let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } let(:service_attrs_list_without_passwords) do - service_fields. - select { |field| field[:type] != 'password' }. - map { |field| field[:name].to_sym} + service_fields + .select { |field| field[:type] != 'password' } + .map { |field| field[:name].to_sym} end let(:service_attrs) do service_attrs_list.inject({}) do |hash, k| diff --git a/spec/support/protected_branches/access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb index 287d6bb13c3..b6341127a76 100644 --- a/spec/support/protected_branches/access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb @@ -1,4 +1,4 @@ -RSpec.shared_examples "protected branches > access control > CE" do +shared_examples "protected branches > access control > CE" do ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do visit namespace_project_protected_branches_path(project.namespace, project) diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 7e35ebb6c97..044c09d5fde 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -11,14 +11,18 @@ RSpec.shared_examples 'slack or mattermost notifications' do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:webhook) } it_behaves_like 'issue tracker service URL attribute', :webhook end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:webhook) } end @@ -104,9 +108,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'uses the username as an option for slack when configured' do allow(chat_service).to receive(:username).and_return(username) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). - and_return( + expect(Slack::Notifier).to receive(:new) + .with(webhook_url, username: username) + .and_return( double(:slack_service).as_null_object ) @@ -115,9 +119,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do it 'uses the channel as an option when it is configured' do allow(chat_service).to receive(:channel).and_return(channel) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: channel). - and_return( + expect(Slack::Notifier).to receive(:new) + .with(webhook_url, channel: channel) + .and_return( double(:slack_service).as_null_object ) chat_service.execute(push_sample_data) @@ -127,9 +131,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do it "uses the right channel for push event" do chat_service.update_attributes(push_channel: "random") - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( + expect(Slack::Notifier).to receive(:new) + .with(webhook_url, channel: "random") + .and_return( double(:slack_service).as_null_object ) @@ -139,9 +143,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do it "uses the right channel for merge request event" do chat_service.update_attributes(merge_request_channel: "random") - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( + expect(Slack::Notifier).to receive(:new) + .with(webhook_url, channel: "random") + .and_return( double(:slack_service).as_null_object ) @@ -151,9 +155,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do it "uses the right channel for issue event" do chat_service.update_attributes(issue_channel: "random") - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( + expect(Slack::Notifier).to receive(:new) + .with(webhook_url, channel: "random") + .and_return( double(:slack_service).as_null_object ) @@ -163,9 +167,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do it "uses the right channel for wiki event" do chat_service.update_attributes(wiki_page_channel: "random") - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( + expect(Slack::Notifier).to receive(:new) + .with(webhook_url, channel: "random") + .and_return( double(:slack_service).as_null_object ) @@ -182,9 +186,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do note_data = Gitlab::DataBuilder::Note.build(issue_note, user) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( + expect(Slack::Notifier).to receive(:new) + .with(webhook_url, channel: "random") + .and_return( double(:slack_service).as_null_object ) diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index b39a23bd18a..48f454c7187 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -5,8 +5,8 @@ module StubConfiguration # Stubbing both of these because we're not yet consistent with how we access # current application settings allow_any_instance_of(ApplicationSetting).to receive_messages(messages) - allow(Gitlab::CurrentSettings.current_application_settings). - to receive_messages(messages) + allow(Gitlab::CurrentSettings.current_application_settings) + .to receive_messages(messages) end def stub_config_setting(messages) diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb index 18597b5c71f..2999bcd9fb1 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/stub_env.rb @@ -5,3 +5,11 @@ module StubENV allow(ENV).to receive(:[]).with(key).and_return(value) end end + +# It's possible that the state of the class variables are not reset across +# test runs. +RSpec.configure do |config| + config.after(:each) do + @env_already_stubbed = nil + end +end diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index ded2d593059..78a2ff73746 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -68,22 +68,22 @@ module StubGitlabCalls def stub_session f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json')) - stub_request(:post, "#{gitlab_url}api/v3/session.json"). - with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", - headers: { 'Content-Type' => 'application/json' }). - to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' }) + stub_request(:post, "#{gitlab_url}api/v3/session.json") + .with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", + headers: { 'Content-Type' => 'application/json' }) + .to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_user f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json')) - stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type' => 'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) + stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz") + .with(headers: { 'Content-Type' => 'application/json' }) + .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) - stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token"). - with(headers: { 'Content-Type' => 'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) + stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token") + .with(headers: { 'Content-Type' => 'application/json' }) + .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_project_8 @@ -99,21 +99,21 @@ module StubGitlabCalls def stub_projects f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) - stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type' => 'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) + stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") + .with(headers: { 'Content-Type' => 'application/json' }) + .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_projects_owned - stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type' => 'application/json' }). - to_return(status: 200, body: "", headers: {}) + stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") + .with(headers: { 'Content-Type' => 'application/json' }) + .to_return(status: 200, body: "", headers: {}) end def stub_ci_enable - stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type' => 'application/json' }). - to_return(status: 200, body: "", headers: {}) + stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz") + .with(headers: { 'Content-Type' => 'application/json' }) + .to_return(status: 200, body: "", headers: {}) end def project_hash_array diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 3f472e59c49..1c5267c290b 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -83,13 +83,13 @@ module TestEnv end def disable_mailer - allow_any_instance_of(NotificationService).to receive(:mailer). - and_return(double.as_null_object) + allow_any_instance_of(NotificationService).to receive(:mailer) + .and_return(double.as_null_object) end def enable_mailer - allow_any_instance_of(NotificationService).to receive(:mailer). - and_call_original + allow_any_instance_of(NotificationService).to receive(:mailer) + .and_call_original end def disable_pre_receive diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb index b407b8097d2..0fa74f911f6 100644 --- a/spec/support/time_tracking_shared_examples.rb +++ b/spec/support/time_tracking_shared_examples.rb @@ -54,7 +54,7 @@ shared_examples 'issuable time tracker' do it 'shows the help state when icon is clicked' do page.within '.time-tracking-component-wrap' do find('.help-button').click - expect(page).to have_content 'Track time with slash commands' + expect(page).to have_content 'Track time with quick actions' expect(page).to have_content 'Learn more' end end @@ -64,7 +64,7 @@ shared_examples 'issuable time tracker' do find('.help-button').click find('.close-help-button').click - expect(page).not_to have_content 'Track time with slash commands' + expect(page).not_to have_content 'Track time with quick actions' expect(page).not_to have_content 'Learn more' end end @@ -78,8 +78,8 @@ shared_examples 'issuable time tracker' do end end -def submit_time(slash_command) - fill_in 'note[note]', with: slash_command +def submit_time(quick_action) + fill_in 'note[note]', with: quick_action find('.js-comment-submit-button').trigger('click') wait_for_requests end diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index 7cf5a65eeed..1986d202c4a 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -31,7 +31,9 @@ end shared_examples 'user login operation with unique ip limit' do include_context 'unique ips sign in limit' do - before { current_application_settings.update!(unique_ips_limit_per_user: 1) } + before do + current_application_settings.update!(unique_ips_limit_per_user: 1) + end it 'allows user authenticating from the same ip' do expect { operation_from_ip('ip') }.not_to raise_error @@ -47,7 +49,9 @@ end shared_examples 'user login request with unique ip limit' do |success_status = 200| include_context 'unique ips sign in limit' do - before { current_application_settings.update!(unique_ips_limit_per_user: 1) } + before do + current_application_settings.update!(unique_ips_limit_per_user: 1) + end it 'allows user authenticating from the same ip' do expect(request_from_ip('ip')).to have_http_status(success_status) diff --git a/spec/support/unpack-gitlab-git-test b/spec/support/unpack-gitlab-git-test new file mode 100755 index 00000000000..d5b4912457d --- /dev/null +++ b/spec/support/unpack-gitlab-git-test @@ -0,0 +1,38 @@ +#!/usr/bin/env ruby +require 'fileutils' + +REPO = 'spec/support/gitlab-git-test.git'.freeze +PACK_DIR = REPO + '/objects/pack' +GIT = %W[git --git-dir=#{REPO}].freeze +BASE_PACK = 'pack-691247af2a6acb0b63b73ac0cb90540e93614043'.freeze + +def main + unpack + # We want to store the refs in a packed-refs file because if we don't + # they can get mangled by filesystems. + abort unless system(*GIT, *%w[pack-refs --all]) + abort unless system(*GIT, 'fsck') +end + +# We don't want contributors to commit new pack files because those +# create unnecessary churn. +def unpack + pack_files = Dir[File.join(PACK_DIR, '*')].reject do |pack| + pack.start_with?(File.join(PACK_DIR, BASE_PACK)) + end + return if pack_files.empty? + + pack_files.each do |pack| + unless pack.end_with?('.pack') + FileUtils.rm(pack) + next + end + + File.open(pack, 'rb') do |open_pack| + File.unlink(pack) + abort unless system(*GIT, 'unpack-objects', in: open_pack) + end + end +end + +main diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb index 365c34448ac..1490287681b 100644 --- a/spec/support/update_invalid_issuable.rb +++ b/spec/support/update_invalid_issuable.rb @@ -21,8 +21,8 @@ shared_examples 'update invalid issuable' do |klass| context 'when updating causes conflicts' do before do - allow_any_instance_of(issuable.class).to receive(:save). - and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) + allow_any_instance_of(issuable.class).to receive(:save) + .and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) end it 'renders edit when format is html' do diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb index e0c59a5c280..eeec3e1d79b 100644 --- a/spec/support/updating_mentions_shared_examples.rb +++ b/spec/support/updating_mentions_shared_examples.rb @@ -2,7 +2,9 @@ RSpec.shared_examples 'updating mentions' do |service_class| let(:mentioned_user) { create(:user) } let(:service_class) { service_class } - before { project.team << [mentioned_user, :developer] } + before do + project.team << [mentioned_user, :developer] + end def update_mentionable(opts) reset_delivered_emails! @@ -15,7 +17,9 @@ RSpec.shared_examples 'updating mentions' do |service_class| end context 'in title' do - before { update_mentionable(title: mentioned_user.to_reference) } + before do + update_mentionable(title: mentioned_user.to_reference) + end it 'emails only the newly-mentioned user' do should_only_email(mentioned_user) @@ -23,7 +27,9 @@ RSpec.shared_examples 'updating mentions' do |service_class| end context 'in description' do - before { update_mentionable(description: mentioned_user.to_reference) } + before do + update_mentionable(description: mentioned_user.to_reference) + end it 'emails only the newly-mentioned user' do should_only_email(mentioned_user) diff --git a/spec/support/user_activities_helpers.rb b/spec/support/user_activities_helpers.rb index f7ca9a31edd..44feb104644 100644 --- a/spec/support/user_activities_helpers.rb +++ b/spec/support/user_activities_helpers.rb @@ -1,7 +1,7 @@ module UserActivitiesHelpers def user_activity(user) - Gitlab::UserActivities.new. - find { |k, _| k == user.id.to_s }&. + Gitlab::UserActivities.new + .find { |k, _| k == user.id.to_s }&. second end end diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb index 05ec9026141..b5c3c0f55b8 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/wait_for_requests.rb @@ -7,7 +7,7 @@ module WaitForRequests def block_and_wait_for_requests_complete Gitlab::Testing::RequestBlockerMiddleware.block_requests! wait_for('pending requests complete') do - Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? + Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? && finished_all_requests? end ensure Gitlab::Testing::RequestBlockerMiddleware.allow_requests! @@ -40,22 +40,16 @@ module WaitForRequests end def finished_all_vue_resource_requests? - page.evaluate_script('window.activeVueResources || 0').zero? + Capybara.page.evaluate_script('window.activeVueResources || 0').zero? end def finished_all_ajax_requests? - return true if page.evaluate_script('typeof jQuery === "undefined"') + return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"') - page.evaluate_script('jQuery.active').zero? + Capybara.page.evaluate_script('jQuery.active').zero? end def javascript_test? Capybara.current_driver == Capybara.javascript_driver end end - -RSpec.configure do |config| - config.after(:each, :js) do - block_and_wait_for_requests_complete - end -end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 0ff1a988a9e..71580a788d0 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -47,24 +47,24 @@ describe 'gitlab:app namespace rake task' do allow(Kernel).to receive(:system).and_return(true) allow(FileUtils).to receive(:cp_r).and_return(true) allow(FileUtils).to receive(:mv).and_return(true) - allow(Rake::Task["gitlab:shell:setup"]). - to receive(:invoke).and_return(true) + allow(Rake::Task["gitlab:shell:setup"]) + .to receive(:invoke).and_return(true) ENV['force'] = 'yes' end let(:gitlab_version) { Gitlab::VERSION } it 'fails on mismatch' do - allow(YAML).to receive(:load_file). - and_return({ gitlab_version: "not #{gitlab_version}" }) + allow(YAML).to receive(:load_file) + .and_return({ gitlab_version: "not #{gitlab_version}" }) - expect { run_rake_task('gitlab:backup:restore') }. - to raise_error(SystemExit) + expect { run_rake_task('gitlab:backup:restore') } + .to raise_error(SystemExit) end it 'invokes restoration on match' do - allow(YAML).to receive(:load_file). - and_return({ gitlab_version: gitlab_version }) + allow(YAML).to receive(:load_file) + .and_return({ gitlab_version: gitlab_version }) expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke) expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke) @@ -241,6 +241,10 @@ describe 'gitlab:app namespace rake task' do project_a project_b + # Avoid asking gitaly about the root ref (which will fail beacuse of the + # mocked storages) + allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false) + # We only need a backup of the repositories for this test ENV["SKIP"] = "db,uploads,builds,artifacts,lfs,registry" create_backup @@ -306,8 +310,8 @@ describe 'gitlab:app namespace rake task' do end it 'does not invoke repositories restore' do - allow(Rake::Task['gitlab:shell:setup']). - to receive(:invoke).and_return(true) + allow(Rake::Task['gitlab:shell:setup']) + .to receive(:invoke).and_return(true) allow($stdout).to receive :write expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 4a636decafd..d42d2423f15 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -20,8 +20,8 @@ describe 'gitlab:gitaly namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do - expect_any_instance_of(Object). - to receive(:checkout_or_clone_version).and_raise 'Git error' + expect_any_instance_of(Object) + .to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error' end @@ -33,8 +33,8 @@ describe 'gitlab:gitaly namespace rake task' do end it 'calls checkout_or_clone_version with the right arguments' do - expect_any_instance_of(Object). - to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) + expect_any_instance_of(Object) + .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -79,16 +79,24 @@ describe 'gitlab:gitaly namespace rake task' do describe 'storage_config' do it 'prints storage configuration in a TOML format' do config = { - 'default' => { 'path' => '/path/to/default' }, - 'nfs_01' => { 'path' => '/path/to/nfs_01' } + 'default' => { + 'path' => '/path/to/default', + 'gitaly_address' => 'unix:/path/to/my.socket' + }, + 'nfs_01' => { + 'path' => '/path/to/nfs_01', + 'gitaly_address' => 'unix:/path/to/my.socket' + } } allow(Gitlab.config.repositories).to receive(:storages).and_return(config) + allow(Rails.env).to receive(:test?).and_return(false) expected_output = '' Timecop.freeze do expected_output = <<~TOML # Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)} # This is in TOML format suitable for use in Gitaly's config.toml file. + socket_path = "/path/to/my.socket" [[storage]] name = "default" path = "/path/to/default" @@ -98,8 +106,8 @@ describe 'gitlab:gitaly namespace rake task' do TOML end - expect { run_rake_task('gitlab:gitaly:storage_config')}. - to output(expected_output).to_stdout + expect { run_rake_task('gitlab:gitaly:storage_config')} + .to output(expected_output).to_stdout parsed_output = TOML.parse(expected_output) config.each do |name, params| diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index 3d9ba7cdc6f..91cc684d032 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -60,8 +60,8 @@ describe Gitlab::TaskHelpers do describe '#clone_repo' do it 'clones the repo in the target dir' do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}]) + expect(subject) + .to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{clone_path}]) subject.clone_repo(repo, clone_path) end @@ -69,10 +69,10 @@ describe Gitlab::TaskHelpers do describe '#checkout_version' do it 'clones the repo in the target dir' do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet]) - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}]) + expect(subject) + .to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet]) + expect(subject) + .to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}]) subject.checkout_version(tag, clone_path) end @@ -80,8 +80,8 @@ describe Gitlab::TaskHelpers do describe '#reset_to_version' do it 'resets --hard to the given version' do - expect(subject). - to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}]) + expect(subject) + .to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}]) subject.reset_to_version(tag, clone_path) end diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb index 63d1cf2bbe5..1b68f3044a4 100644 --- a/spec/tasks/gitlab/workhorse_rake_spec.rb +++ b/spec/tasks/gitlab/workhorse_rake_spec.rb @@ -20,8 +20,8 @@ describe 'gitlab:workhorse namespace rake task' do context 'when an underlying Git command fail' do it 'aborts and display a help message' do - expect_any_instance_of(Object). - to receive(:checkout_or_clone_version).and_raise 'Git error' + expect_any_instance_of(Object) + .to receive(:checkout_or_clone_version).and_raise 'Git error' expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error' end @@ -33,8 +33,8 @@ describe 'gitlab:workhorse namespace rake task' do end it 'calls checkout_or_clone_version with the right arguments' do - expect_any_instance_of(Object). - to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) + expect_any_instance_of(Object) + .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) run_rake_task('gitlab:workhorse:install', clone_path) end diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb index 8dbf3eecd23..8bd5306ff98 100644 --- a/spec/validators/dynamic_path_validator_spec.rb +++ b/spec/validators/dynamic_path_validator_spec.rb @@ -84,5 +84,14 @@ describe DynamicPathValidator do expect(group.errors[:path]).to include('users is a reserved name') end + + it 'updating to an invalid path is not allowed' do + project = create(:empty_project) + project.path = 'update' + + validator.validate_each(project, :path, 'update') + + expect(project.errors[:path]).to include('update is a reserved name') + end end end diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb index 1397bfa5864..9adbb0476be 100644 --- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb +++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb @@ -31,7 +31,7 @@ describe 'devise/shared/_signin_box' do def enable_crowd allow(view).to receive(:form_based_providers).and_return([:crowd]) allow(view).to receive(:crowd_enabled?).and_return(true) - allow(view).to receive(:omniauth_authorize_path).with(:user, :crowd). - and_return('/crowd') + allow(view).to receive(:omniauth_authorize_path).with(:user, :crowd) + .and_return('/crowd') end end diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/profiles/show.html.haml_spec.rb new file mode 100644 index 00000000000..e89a8cb9626 --- /dev/null +++ b/spec/views/profiles/show.html.haml_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe 'profiles/show' do + let(:user) { create(:user) } + + before do + assign(:user, user) + allow(controller).to receive(:current_user).and_return(user) + end + + context 'when the profile page is opened' do + it 'displays the correct elements' do + render + + expect(rendered).to have_field('user_name', user.name) + expect(rendered).to have_field('user_id', user.id) + end + end +end diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb index 122075cc10e..92b4aa12d49 100644 --- a/spec/views/projects/commit/show.html.haml_spec.rb +++ b/spec/views/projects/commit/show.html.haml_spec.rb @@ -21,24 +21,26 @@ describe 'projects/commit/show.html.haml', :view do context 'inline diff view' do before do allow(view).to receive(:diff_view).and_return(:inline) + allow(view).to receive(:diff_view).and_return(:inline) render end - it 'keeps container-limited' do - expect(rendered).not_to have_selector('.limit-container-width') + it 'has limited width' do + expect(rendered).to have_selector('.limit-container-width') end end context 'parallel diff view' do before do allow(view).to receive(:diff_view).and_return(:parallel) + allow(view).to receive(:fluid_layout).and_return(true) render end it 'spans full width' do - expect(rendered).to have_selector('.limit-container-width') + expect(rendered).not_to have_selector('.limit-container-width') end end end diff --git a/spec/views/projects/diffs/_viewer.html.haml_spec.rb b/spec/views/projects/diffs/_viewer.html.haml_spec.rb new file mode 100644 index 00000000000..32469202508 --- /dev/null +++ b/spec/views/projects/diffs/_viewer.html.haml_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe 'projects/diffs/_viewer.html.haml', :view do + include FakeBlobHelpers + + let(:project) { create(:project, :repository) } + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + let(:viewer_class) do + Class.new(DiffViewer::Base) do + include DiffViewer::Rich + + self.partial_name = 'text' + end + end + + let(:viewer) { viewer_class.new(diff_file) } + + before do + assign(:project, project) + + controller.params[:controller] = 'projects/commit' + controller.params[:action] = 'show' + controller.params[:namespace_id] = project.namespace.to_param + controller.params[:project_id] = project.to_param + controller.params[:id] = commit.id + end + + def render_view + render partial: 'projects/diffs/viewer', locals: { viewer: viewer } + end + + context 'when there is a render error' do + before do + allow(viewer).to receive(:render_error).and_return(:too_large) + end + + it 'renders the error' do + render_view + + expect(view).to render_template('projects/diffs/_render_error') + end + end + + context 'when the viewer is collapsed' do + before do + allow(diff_file).to receive(:collapsed?).and_return(true) + end + + it 'renders the collapsed view' do + render_view + + expect(view).to render_template('projects/diffs/_collapsed') + end + end + + context 'when there is no render error' do + it 'prepares the viewer' do + expect(viewer).to receive(:prepare!) + + render_view + end + + it 'renders the viewer' do + render_view + + expect(view).to render_template('projects/diffs/viewers/_text') + end + end +end diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb new file mode 100644 index 00000000000..e56c0f6be03 --- /dev/null +++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'projects/notes/_more_actions_dropdown', :view do + let(:author_user) { create(:user) } + let(:not_author_user) { create(:user) } + + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let!(:note) { create(:note_on_issue, author: author_user, noteable: issue, project: project) } + + before do + assign(:project, project) + end + + it 'shows Report as abuse button if not editable and not current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note + + expect(rendered).to have_link('Report as abuse') + end + + it 'does not show the More actions button if not editable and current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: false, note: note + + expect(rendered).not_to have_selector('.dropdown.more-actions') + end + + it 'shows Report as abuse, Edit and Delete buttons if editable and not current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note + + expect(rendered).to have_link('Report as abuse') + expect(rendered).to have_button('Edit comment') + expect(rendered).to have_link('Delete comment') + end + + it 'shows Edit and Delete buttons if editable and current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: true, note: note + + expect(rendered).to have_button('Edit comment') + expect(rendered).to have_link('Delete comment') + end +end diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb index d7d0a5bf56a..cae6bee2776 100644 --- a/spec/views/shared/notes/_form.html.haml_spec.rb +++ b/spec/views/shared/notes/_form.html.haml_spec.rb @@ -20,8 +20,8 @@ describe 'shared/notes/_form' do context "with a note on #{noteable}" do let(:note) { build(:"note_on_#{noteable}", project: project) } - it 'says that markdown and slash commands are supported' do - expect(rendered).to have_content('Markdown and slash commands are supported') + it 'says that markdown and quick actions are supported' do + expect(rendered).to have_content('Markdown and quick actions are supported') end end end @@ -29,7 +29,7 @@ describe 'shared/notes/_form' do context 'with a note on a commit' do let(:note) { build(:note_on_commit, project: project) } - it 'says that only markdown is supported, not slash commands' do + it 'says that only markdown is supported, not quick actions' do expect(rendered).to have_content('Markdown is supported') end end diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb index 0d742ae9dc7..85939429feb 100644 --- a/spec/workers/background_migration_worker_spec.rb +++ b/spec/workers/background_migration_worker_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe BackgroundMigrationWorker do describe '.perform' do it 'performs a background migration' do - expect(Gitlab::BackgroundMigration). - to receive(:perform). - with('Foo', [10, 20]) + expect(Gitlab::BackgroundMigration) + .to receive(:perform) + .with('Foo', [10, 20]) described_class.new.perform('Foo', [10, 20]) end diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb index 5912dd76262..36594515005 100644 --- a/spec/workers/delete_user_worker_spec.rb +++ b/spec/workers/delete_user_worker_spec.rb @@ -5,15 +5,15 @@ describe DeleteUserWorker do let!(:current_user) { create(:user) } it "calls the DeleteUserWorker with the params it was given" do - expect_any_instance_of(Users::DestroyService).to receive(:execute). - with(user, {}) + expect_any_instance_of(Users::DestroyService).to receive(:execute) + .with(user, {}) described_class.new.perform(current_user.id, user.id) end it "uses symbolized keys" do - expect_any_instance_of(Users::DestroyService).to receive(:execute). - with(user, test: "test") + expect_any_instance_of(Users::DestroyService).to receive(:execute) + .with(user, test: "test") described_class.new.perform(current_user.id, user.id, "test" => "test") end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index a0ed85cc0b3..5b6b38e0f76 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -71,7 +71,9 @@ describe EmailsOnPushWorker do end context "when there are no errors in sending" do - before { perform } + before do + perform + end it "sends a mail with the correct subject" do expect(email.subject).to include('adds bar folder and branch-test text file') diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index fc9adf47c1e..30908534eb3 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -5,8 +5,8 @@ describe 'Every Sidekiq worker' do root = Rails.root.join('app', 'workers') concerns = root.join('concerns').to_s - workers = Dir[root.join('**', '*.rb')]. - reject { |path| path.start_with?(concerns) } + workers = Dir[root.join('**', '*.rb')] + .reject { |path| path.start_with?(concerns) } workers.map do |path| ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '') @@ -22,9 +22,9 @@ describe 'Every Sidekiq worker' do end it 'uses the cronjob queue when the worker runs as a cronjob' do - cron_workers = Settings.cron_jobs. - map { |job_name, options| options['job_class'].constantize }. - to_set + cron_workers = Settings.cron_jobs + .map { |job_name, options| options['job_class'].constantize } + .to_set workers.each do |worker| next unless cron_workers.include?(worker) diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb index 73cbadc13d9..b47b4a02a68 100644 --- a/spec/workers/expire_build_artifacts_worker_spec.rb +++ b/spec/workers/expire_build_artifacts_worker_spec.rb @@ -5,10 +5,14 @@ describe ExpireBuildArtifactsWorker do let(:worker) { described_class.new } - before { Sidekiq::Worker.clear_all } + before do + Sidekiq::Worker.clear_all + end describe '#perform' do - before { build } + before do + build + end subject! do Sidekiq::Testing.fake! { worker.perform } diff --git a/spec/workers/expire_pipeline_cache_worker_spec.rb b/spec/workers/expire_pipeline_cache_worker_spec.rb index 28e5b706803..e4f78999489 100644 --- a/spec/workers/expire_pipeline_cache_worker_spec.rb +++ b/spec/workers/expire_pipeline_cache_worker_spec.rb @@ -37,8 +37,8 @@ describe ExpirePipelineCacheWorker do end it 'updates the cached status for a project' do - expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline). - with(pipeline) + expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline) + .with(pipeline) subject.perform(pipeline.id) end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 8c5303b61cc..309b3172da1 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -11,8 +11,8 @@ describe GitGarbageCollectWorker do describe "#perform" do it "flushes ref caches when the task is 'gc'" do expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) - expect(Gitlab::Popen).to receive(:popen). - with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + expect(Gitlab::Popen).to receive(:popen) + .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original @@ -23,7 +23,9 @@ describe GitGarbageCollectWorker do end shared_examples 'gc tasks' do - before { allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) } + before do + allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) + end it 'incremental repack adds a new packfile' do create_objects(project) diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb index 8fdbb35afd0..575361c93d4 100644 --- a/spec/workers/new_note_worker_spec.rb +++ b/spec/workers/new_note_worker_spec.rb @@ -24,8 +24,8 @@ describe NewNoteWorker do let(:unexistent_note_id) { 999 } it 'logs NewNoteWorker process skipping' do - expect(Rails.logger).to receive(:error). - with("NewNoteWorker: couldn't find note with ID=999, skipping job") + expect(Rails.logger).to receive(:error) + .with("NewNoteWorker: couldn't find note with ID=999, skipping job") described_class.new.perform(unexistent_note_id) end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 44163c735ba..cc9bc29c6cc 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -32,7 +32,7 @@ describe PostReceive do context "with an absolute path as the project identifier" do it "searches the project by full path" do - expect(Project).to receive(:find_by_full_path).with(project.full_path).and_call_original + expect(Project).to receive(:find_by_full_path).with(project.full_path, follow_redirects: true).and_call_original described_class.new.perform(pwd(project), key_id, base64_changes) end @@ -89,7 +89,9 @@ describe PostReceive do end context "does not create a Ci::Pipeline" do - before { stub_ci_pipeline_yaml_file(nil) } + before do + stub_ci_pipeline_yaml_file(nil) + end it { expect{ subject }.not_to change{ Ci::Pipeline.count } } end @@ -121,9 +123,9 @@ describe PostReceive do end it "does not run if the author is not in the project" do - allow_any_instance_of(Gitlab::GitPostReceive). - to receive(:identify_using_ssh_key). - and_return(nil) + allow_any_instance_of(Gitlab::GitPostReceive) + .to receive(:identify_using_ssh_key) + .and_return(nil) expect(project).not_to receive(:execute_hooks) diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 4e036285e8c..6ebc94bb544 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -48,11 +48,11 @@ describe ProcessCommitWorker do describe '#process_commit_message' do context 'when pushing to the default branch' do it 'closes issues that should be closed per the commit message' do - allow(commit).to receive(:safe_message). - and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message) + .and_return("Closes #{issue.to_reference}") - expect(worker).to receive(:close_issues). - with(project, user, user, commit, [issue]) + expect(worker).to receive(:close_issues) + .with(project, user, user, commit, [issue]) worker.process_commit_message(project, commit, user, user, true) end @@ -60,8 +60,8 @@ describe ProcessCommitWorker do context 'when pushing to a non-default branch' do it 'does not close any issues' do - allow(commit).to receive(:safe_message). - and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message) + .and_return("Closes #{issue.to_reference}") expect(worker).not_to receive(:close_issues) @@ -102,8 +102,8 @@ describe ProcessCommitWorker do describe '#update_issue_metrics' do it 'updates any existing issue metrics' do - allow(commit).to receive(:safe_message). - and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message) + .and_return("Closes #{issue.to_reference}") worker.update_issue_metrics(commit, user) @@ -113,8 +113,8 @@ describe ProcessCommitWorker do end it "doesn't execute any queries with false conditions" do - allow(commit).to receive(:safe_message). - and_return("Lorem Ipsum") + allow(commit).to receive(:safe_message) + .and_return("Lorem Ipsum") expect { worker.update_issue_metrics(commit, user) }.not_to make_queries_matching(/WHERE (?:1=0|0=1)/) end @@ -128,8 +128,8 @@ describe ProcessCommitWorker do end it 'parses date strings into Time instances' do - commit = worker. - build_commit(project, id: '123', authored_date: Time.now.to_s) + commit = worker + .build_commit(project, id: '123', authored_date: Time.now.to_s) expect(commit.authored_date).to be_an_instance_of(Time) end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index a4ba5f7c943..6b1f2ff3227 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -7,8 +7,8 @@ describe ProjectCacheWorker do describe '#perform' do before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). - and_return(true) + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) + .and_return(true) end context 'with a non-existing project' do @@ -39,9 +39,9 @@ describe ProjectCacheWorker do end it 'refreshes the method caches' do - expect_any_instance_of(Repository).to receive(:refresh_method_caches). - with(%i(readme)). - and_call_original + expect_any_instance_of(Repository).to receive(:refresh_method_caches) + .with(%i(readme)) + .and_call_original worker.perform(project.id, %w(readme)) end @@ -51,9 +51,9 @@ describe ProjectCacheWorker do allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false) allow(MarkupHelper).to receive(:plain?).and_return(true) - expect_any_instance_of(Repository).to receive(:refresh_method_caches). - with(%i(readme)). - and_call_original + expect_any_instance_of(Repository).to receive(:refresh_method_caches) + .with(%i(readme)) + .and_call_original worker.perform(project.id, %w(readme)) end end @@ -63,9 +63,9 @@ describe ProjectCacheWorker do describe '#update_statistics' do context 'when a lease could not be obtained' do it 'does not update the repository size' do - allow(worker).to receive(:try_obtain_lease_for). - with(project.id, :update_statistics). - and_return(false) + allow(worker).to receive(:try_obtain_lease_for) + .with(project.id, :update_statistics) + .and_return(false) expect(statistics).not_to receive(:refresh!) @@ -75,9 +75,9 @@ describe ProjectCacheWorker do context 'when a lease could be obtained' do it 'updates the project statistics' do - allow(worker).to receive(:try_obtain_lease_for). - with(project.id, :update_statistics). - and_return(true) + allow(worker).to receive(:try_obtain_lease_for) + .with(project.id, :update_statistics) + .and_return(true) expect(statistics).to receive(:refresh!) .with(only: %i(repository_size)) diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb index 7040d5ef81c..b8b65ead9b3 100644 --- a/spec/workers/propagate_service_template_worker_spec.rb +++ b/spec/workers/propagate_service_template_worker_spec.rb @@ -15,8 +15,8 @@ describe PropagateServiceTemplateWorker do end before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). - and_return(true) + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) + .and_return(true) end describe '#perform' do diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 6ea5569b438..d9e9409840f 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -35,11 +35,11 @@ describe RepositoryForkWorker do fork_project.namespace.full_path ).and_return(true) - expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). - and_call_original + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) + .and_call_original - expect_any_instance_of(Repository).to receive(:expire_exists_cache). - and_call_original + expect_any_instance_of(Repository).to receive(:expire_exists_cache) + .and_call_original subject.perform(project.id, '/test/path', project.full_path, fork_project.namespace.full_path) diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 9c277c501f1..6b30dabc80e 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -8,8 +8,8 @@ describe RepositoryImportWorker do describe '#perform' do context 'when the import was successful' do it 'imports a project' do - expect_any_instance_of(Projects::ImportService).to receive(:execute). - and_return({ status: :ok }) + expect_any_instance_of(Projects::ImportService).to receive(:execute) + .and_return({ status: :ok }) expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) expect_any_instance_of(Project).to receive(:import_finish) diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index 8434b0c8e5b..549635f7f33 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -34,7 +34,9 @@ describe StuckCiJobsWorker do let(:status) { 'pending' } context 'when job is not stuck' do - before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false) } + before do + allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false) + end context 'when job was not updated for more than 1 day ago' do let(:updated_at) { 2.days.ago } @@ -53,7 +55,9 @@ describe StuckCiJobsWorker do end context 'when job is stuck' do - before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true) } + before do + allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true) + end context 'when job was not updated for more than 1 hour ago' do let(:updated_at) { 2.hours.ago } @@ -93,7 +97,9 @@ describe StuckCiJobsWorker do let(:status) { 'running' } let(:updated_at) { 2.days.ago } - before { job.project.update(pending_delete: true) } + before do + job.project.update(pending_delete: true) + end it 'does not drop job' do expect_any_instance_of(Ci::Build).not_to receive(:drop) diff --git a/vendor/Dockerfile/Binary-alpine.Dockerfile b/vendor/Dockerfile/Binary-alpine.Dockerfile new file mode 100644 index 00000000000..5a9eb2b4716 --- /dev/null +++ b/vendor/Dockerfile/Binary-alpine.Dockerfile @@ -0,0 +1,14 @@ +# This Dockerfile installs a compiled binary into a bare system. +# You must either commit your compiled binary into source control (not recommended) +# or build the binary first as part of a CI/CD pipeline. + +FROM alpine:3.5 + +# We'll likely need to add SSL root certificates +RUN apk --no-cache add ca-certificates + +WORKDIR /usr/local/bin + +# Change `app` to whatever your binary is called +Add app . +CMD ["./app"] diff --git a/vendor/Dockerfile/Binary-scratch.Dockerfile b/vendor/Dockerfile/Binary-scratch.Dockerfile new file mode 100644 index 00000000000..5e2de2ead61 --- /dev/null +++ b/vendor/Dockerfile/Binary-scratch.Dockerfile @@ -0,0 +1,17 @@ +# This Dockerfile installs a compiled binary into an image with no system at all. +# You must either commit your compiled binary into source control (not recommended) +# or build the binary first as part of a CI/CD pipeline. +# Your binary must be statically compiled with no dynamic dependencies on system libraries. +# e.g. for Docker: +# CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . + +FROM scratch + +# Since we started from scratch, we'll likely need to add SSL root certificates +ADD /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +WORKDIR /usr/local/bin + +# Change `app` to whatever your binary is called +Add app . +CMD ["./app"] diff --git a/vendor/Dockerfile/Binary.Dockerfile b/vendor/Dockerfile/Binary.Dockerfile new file mode 100644 index 00000000000..e7d560da9ac --- /dev/null +++ b/vendor/Dockerfile/Binary.Dockerfile @@ -0,0 +1,11 @@ +# This Dockerfile installs a compiled binary into a bare system. +# You must either commit your compiled binary into source control (not recommended) +# or build the binary first as part of a CI/CD pipeline. + +FROM buildpack-deps:jessie + +WORKDIR /usr/local/bin + +# Change `app` to whatever your binary is called +Add app . +CMD ["./app"] diff --git a/vendor/Dockerfile/Golang-alpine.Dockerfile b/vendor/Dockerfile/Golang-alpine.Dockerfile new file mode 100644 index 00000000000..0287315219b --- /dev/null +++ b/vendor/Dockerfile/Golang-alpine.Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.8-alpine AS builder + +WORKDIR /usr/src/app + +COPY . . +RUN go-wrapper download +RUN go build -v + +FROM alpine:3.5 + +# We'll likely need to add SSL root certificates +RUN apk --no-cache add ca-certificates + +WORKDIR /usr/local/bin + +COPY --from=builder /usr/src/app/app . +CMD ["./app"] diff --git a/vendor/Dockerfile/Golang-scratch.Dockerfile b/vendor/Dockerfile/Golang-scratch.Dockerfile new file mode 100644 index 00000000000..9057a2d0e51 --- /dev/null +++ b/vendor/Dockerfile/Golang-scratch.Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.8-alpine AS builder + +# We'll likely need to add SSL root certificates +RUN apk --no-cache add ca-certificates + +WORKDIR /usr/src/app + +COPY . . +RUN go-wrapper download +RUN CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix cgo -o app . + +FROM scratch + +# Since we started from scratch, we'll copy the SSL root certificates from the builder +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +WORKDIR /usr/local/bin + +COPY --from=builder /usr/src/app/app . +CMD ["./app"] diff --git a/vendor/Dockerfile/Golang.Dockerfile b/vendor/Dockerfile/Golang.Dockerfile new file mode 100644 index 00000000000..ec94914be19 --- /dev/null +++ b/vendor/Dockerfile/Golang.Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.8 AS builder + +WORKDIR /usr/src/app + +COPY . . +RUN go-wrapper download +RUN go build -v + +FROM buildpack-deps:jessie + +WORKDIR /usr/local/bin + +COPY --from=builder /usr/src/app/app . +CMD ["./app"] diff --git a/vendor/Dockerfile/Node-alpine.Dockerfile b/vendor/Dockerfile/Node-alpine.Dockerfile new file mode 100644 index 00000000000..9776b1336b5 --- /dev/null +++ b/vendor/Dockerfile/Node-alpine.Dockerfile @@ -0,0 +1,14 @@ +FROM node:7.9-alpine + +WORKDIR /usr/src/app + +ARG NODE_ENV +ENV NODE_ENV $NODE_ENV +COPY package.json /usr/src/app/ +RUN npm install && npm cache clean +COPY . /usr/src/app + +CMD [ "npm", "start" ] + +# replace this with your application's default port +EXPOSE 8888 diff --git a/vendor/Dockerfile/Node.Dockerfile b/vendor/Dockerfile/Node.Dockerfile new file mode 100644 index 00000000000..7e936d5e887 --- /dev/null +++ b/vendor/Dockerfile/Node.Dockerfile @@ -0,0 +1,14 @@ +FROM node:7.9 + +WORKDIR /usr/src/app + +ARG NODE_ENV +ENV NODE_ENV $NODE_ENV +COPY package.json /usr/src/app/ +RUN npm install && npm cache clean +COPY . /usr/src/app + +CMD [ "npm", "start" ] + +# replace this with your application's default port +EXPOSE 8888 diff --git a/vendor/Dockerfile/Ruby-alpine.Dockerfile b/vendor/Dockerfile/Ruby-alpine.Dockerfile new file mode 100644 index 00000000000..9db4e2130f2 --- /dev/null +++ b/vendor/Dockerfile/Ruby-alpine.Dockerfile @@ -0,0 +1,24 @@ +FROM ruby:2.4-alpine + +# Edit with nodejs, mysql-client, postgresql-client, sqlite3, etc. for your needs. +# Or delete entirely if not needed. +RUN apk --no-cache add nodejs postgresql-client + +# throw errors if Gemfile has been modified since Gemfile.lock +RUN bundle config --global frozen 1 + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY Gemfile Gemfile.lock /usr/src/app/ +RUN bundle install + +COPY . /usr/src/app + +# For Sinatra +#EXPOSE 4567 +#CMD ["ruby", "./config.rb"] + +# For Rails +EXPOSE 3000 +CMD ["rails", "server"] diff --git a/vendor/Dockerfile/Ruby.Dockerfile b/vendor/Dockerfile/Ruby.Dockerfile new file mode 100644 index 00000000000..feb880ee4b2 --- /dev/null +++ b/vendor/Dockerfile/Ruby.Dockerfile @@ -0,0 +1,27 @@ +FROM ruby:2.4 + +# Edit with nodejs, mysql-client, postgresql-client, sqlite3, etc. for your needs. +# Or delete entirely if not needed. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + nodejs \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# throw errors if Gemfile has been modified since Gemfile.lock +RUN bundle config --global frozen 1 + +WORKDIR /usr/src/app + +COPY Gemfile Gemfile.lock /usr/src/app/ +RUN bundle install -j $(nproc) + +COPY . /usr/src/app + +# For Sinatra +#EXPOSE 4567 +#CMD ["ruby", "./config.rb"] + +# For Rails +EXPOSE 3000 +CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/vendor/gitignore/Global/Archives.gitignore b/vendor/gitignore/Global/Archives.gitignore index f440b808d98..43fd5582f91 100644 --- a/vendor/gitignore/Global/Archives.gitignore +++ b/vendor/gitignore/Global/Archives.gitignore @@ -12,11 +12,11 @@ *.lzma *.cab -#packing-only formats +# Packing-only formats *.iso *.tar -#package management formats +# Package management formats *.dmg *.xpi *.gem diff --git a/vendor/gitignore/Global/JEnv.gitignore b/vendor/gitignore/Global/JEnv.gitignore new file mode 100644 index 00000000000..d838300ad5e --- /dev/null +++ b/vendor/gitignore/Global/JEnv.gitignore @@ -0,0 +1,5 @@ +# JEnv local Java version configuration file +.java-version + +# Used by previous versions of JEnv +.jenv-version diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore index 95ff2244c99..86c3fa455aa 100644 --- a/vendor/gitignore/Global/SublimeText.gitignore +++ b/vendor/gitignore/Global/SublimeText.gitignore @@ -1,16 +1,16 @@ -# cache files for sublime text +# Cache files for Sublime Text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache -# workspace files are user-specific +# Workspace files are user-specific *.sublime-workspace -# project files should be checked into the repository, unless a significant -# proportion of contributors will probably not be using SublimeText +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text # *.sublime-project -# sftp configuration file +# SFTP configuration file sftp-config.json # Package control specific files diff --git a/vendor/gitignore/Global/Vagrant.gitignore b/vendor/gitignore/Global/Vagrant.gitignore index a977916f658..93987ca00ec 100644 --- a/vendor/gitignore/Global/Vagrant.gitignore +++ b/vendor/gitignore/Global/Vagrant.gitignore @@ -1 +1,5 @@ +# General .vagrant/ + +# Log files (if you are creating logs in debug mode, uncomment this) +# *.logs diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore index 42e7afc1005..6d21783d471 100644 --- a/vendor/gitignore/Global/Vim.gitignore +++ b/vendor/gitignore/Global/Vim.gitignore @@ -1,12 +1,14 @@ -# swap +# Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-v][a-z] [._]sw[a-p] -# session + +# Session Session.vim -# temporary + +# Temporary .netrwhist *~ -# auto-generated tag files +# Auto-generated tag files tags diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore index ba26afd9653..dff26a9ab70 100644 --- a/vendor/gitignore/Global/Windows.gitignore +++ b/vendor/gitignore/Global/Windows.gitignore @@ -3,6 +3,9 @@ Thumbs.db ehthumbs.db ehthumbs_vista.db +# Dump file +*.stackdump + # Folder config file Desktop.ini diff --git a/vendor/gitignore/Global/macOS.gitignore b/vendor/gitignore/Global/macOS.gitignore index 5972fe50f66..9d1061e8bc4 100644 --- a/vendor/gitignore/Global/macOS.gitignore +++ b/vendor/gitignore/Global/macOS.gitignore @@ -1,3 +1,4 @@ +# General *.DS_Store .AppleDouble .LSOverride diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore index 768d5f400bb..113294a5f18 100644 --- a/vendor/gitignore/Python.gitignore +++ b/vendor/gitignore/Python.gitignore @@ -8,7 +8,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -43,7 +42,7 @@ htmlcov/ .cache nosetests.xml coverage.xml -*,cover +*.cover .hypothesis/ # Translations @@ -79,11 +78,10 @@ celerybeat-schedule # SageMath parsed files *.sage.py -# dotenv +# Environments .env - -# virtualenv .venv +env/ venv/ ENV/ diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore index 6732e72091c..5fa47c5a1f2 100644 --- a/vendor/gitignore/Qt.gitignore +++ b/vendor/gitignore/Qt.gitignore @@ -12,6 +12,9 @@ # Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp /.qmake.cache /.qmake.stash *.pro.user @@ -26,6 +29,11 @@ ui_*.h Makefile* *build-* + +# Qt unit tests +target_wrapper.* + + # QtCreator *.autosave diff --git a/vendor/gitignore/SugarCRM.gitignore b/vendor/gitignore/SugarCRM.gitignore index e9270205fd5..6a183d1c748 100644 --- a/vendor/gitignore/SugarCRM.gitignore +++ b/vendor/gitignore/SugarCRM.gitignore @@ -6,7 +6,7 @@ # the misuse of the repository as backup replacement. # For development the cache directory can be safely ignored and # therefore it is ignored. -/cache/ +/cache/* !/cache/index.html # Ignore some files and directories from the custom directory. /custom/history/ @@ -22,6 +22,6 @@ # Logs files can safely be ignored. *.log # Ignore the new upload directories. -/upload/ +/upload/* !/upload/index.html /upload_backup/ diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 940794e60f2..22fd88a55a3 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -42,6 +42,9 @@ TestResult.xml [Rr]eleasePS/ dlldata.c +# Benchmark Results +BenchmarkDotNet.Artifacts/ + # .NET Core project.lock.json project.fragment.lock.json @@ -183,6 +186,7 @@ AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt +*.appx # Visual Studio cache files # files ending in .cache can be ignored @@ -278,6 +282,9 @@ __pycache__/ # tools/** # !tools/packages.config +# Tabs Studio +*.tss + # Telerik's JustMock configuration file *.jmconfig diff --git a/vendor/gitlab-ci-yml/.gitlab-ci.yml b/vendor/gitlab-ci-yml/.gitlab-ci.yml index 18b14554887..e2a55163682 100644 --- a/vendor/gitlab-ci-yml/.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: ruby:2.3-alpine +image: ruby:2.4-alpine test: - script: ruby verify_templates.rb + script: ./verify_templates.rb diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml index 37e44735f7c..02cfab3a5b2 100644 --- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml @@ -4,7 +4,7 @@ image: "crystallang/crystal:latest" # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service # services: # - mysql:latest # - redis:latest diff --git a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml index 5ded2f5ce76..57afcbbe8b5 100644 --- a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml @@ -4,7 +4,7 @@ image: python:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - postgres:latest diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml index 40648bcd3de..5b6af7be8c4 100644 --- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml @@ -6,8 +6,8 @@ services: build: stage: build + before_script: + - docker login -u "$CI_REGISTRY_USER" -p "CI_REGISTRY_PASSWORD" $CI_REGISTRY script: - - export IMAGE_TAG=$(echo -en $CI_COMMIT_REF_NAME | tr -c '[:alnum:]_.-' '-') - - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY - - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" . - - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG" + - docker build --pull -t "$CI_REGISTRY_IMAGE:CI_COMMIT_REF_SLUG" . + - docker push "$CI_REGISTRY_IMAGE:CI_COMMIT_REF_SLUG" diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml index 981a77497e2..cf9c731637c 100644 --- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml @@ -2,7 +2,7 @@ image: elixir:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - redis:latest diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml index 0d6a6eddc97..434de4f055a 100644 --- a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml @@ -4,7 +4,7 @@ image: php:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest diff --git a/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml index e5bce3503f3..41de1458582 100644 --- a/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml @@ -4,7 +4,7 @@ image: node:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - redis:latest diff --git a/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml index bc36a4e6966..7abfaf53e8e 100644 --- a/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml @@ -3,7 +3,7 @@ # # JBake https://jbake.org/ is a Java based, open source, static site/blog generator for developers & designers # -# This yml works with jBake 2.4.0 +# This yml works with jBake 2.5.1 # Feel free to change JBAKE_VERSION version # # HowTo at: https://jorge.aguilera.gitlab.io/howtojbake/ @@ -11,12 +11,12 @@ image: java:8 variables: - JBAKE_VERSION: 2.4.0 + JBAKE_VERSION: 2.5.1 # We use SDKMan as tool for managing versions before_script: - - apt-get update -qq && apt-get install -y -qq unzip + - apt-get update -qq && apt-get install -y -qq unzip zip - curl -sSL https://get.sdkman.io | bash - echo sdkman_auto_answer=true > /root/.sdkman/etc/config - source /root/.sdkman/bin/sdkman-init.sh diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml index 08b57c8c0ac..4e181e85451 100644 --- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml @@ -4,7 +4,7 @@ image: "ruby:2.3" # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service services: - mysql:latest - redis:latest diff --git a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml index ae3f7405ea3..7810121c350 100644 --- a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml @@ -4,7 +4,7 @@ image: "scorpil/rust:stable" # Optional: Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service #services: # - mysql:latest # - redis:latest diff --git a/vendor/licenses.csv b/vendor/licenses.csv index a8e7f5e3ea9..5dcac5947a1 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -2,7 +2,7 @@ RedCloth,4.3.2,MIT abbrev,1.0.9,ISC accepts,1.3.3,MIT ace-rails-ap,4.1.2,MIT -acorn,4.0.11,MIT +acorn,5.0.3,MIT acorn-dynamic-import,2.0.1,MIT acorn-jsx,3.0.1,MIT actionmailer,4.2.8,MIT @@ -53,6 +53,7 @@ assert-plus,0.2.0,MIT async,0.2.10,MIT async-each,1.0.1,MIT asynckit,0.4.0,MIT +atomic,1.1.99,Apache 2.0 attr_encrypted,3.0.3,MIT attr_required,1.0.0,MIT autoparse,0.3.3,Apache 2.0 @@ -151,8 +152,9 @@ blob,0.0.4,unknown block-stream,0.0.9,ISC bluebird,3.4.7,MIT bn.js,4.11.6,MIT -body-parser,1.16.0,MIT +body-parser,1.17.2,MIT boom,2.10.1,New BSD +bootsnap,1.0.0,MIT bootstrap-sass,3.3.6,MIT brace-expansion,1.1.6,MIT braces,1.8.5,MIT @@ -222,9 +224,10 @@ compression,1.6.2,MIT compression-webpack-plugin,0.3.2,MIT concat-map,0.0.1,MIT concat-stream,1.6.0,MIT +concurrent-ruby-ext,1.0.5,MIT config-chain,1.1.11,MIT configstore,1.4.0,Simplified BSD -connect,3.5.0,MIT +connect,3.6.2,MIT connect-history-api-fallback,1.3.0,MIT connection_pool,2.2.1,MIT console-browserify,1.1.0,MIT @@ -262,8 +265,9 @@ dashdash,1.14.1,MIT date-now,0.1.4,MIT de-indent,1.0.2,MIT debug,2.6.0,MIT +debugger-ruby_core_source,1.3.8,MIT decamelize,1.2.0,MIT -deckar01-task_list,1.0.6,MIT +deckar01-task_list,2.0.0,MIT deep-extend,0.4.1,MIT deep-is,0.1.3,MIT default-require-extensions,1.0.0,MIT @@ -312,8 +316,8 @@ emojis-list,2.1.0,MIT encodeurl,1.0.1,MIT encryptor,3.0.0,MIT end-of-stream,1.0.0,MIT -engine.io,1.8.2,MIT -engine.io-client,1.8.2,MIT +engine.io,1.8.3,MIT +engine.io-client,1.8.3,MIT engine.io-parser,1.3.2,MIT enhanced-resolve,3.1.0,MIT ent,2.2.0,MIT @@ -349,7 +353,8 @@ esprima,3.1.3,Simplified BSD esrecurse,4.1.0,Simplified BSD estraverse,4.1.1,Simplified BSD esutils,2.0.2,BSD -etag,1.7.0,MIT +et-orbi,1.0.3,MIT +etag,1.8.0,MIT eve-raphael,0.5.0,Apache 2.0 event-emitter,0.3.4,MIT event-stream,3.3.4,MIT @@ -364,12 +369,11 @@ expand-braces,0.1.2,MIT expand-brackets,0.1.5,MIT expand-range,1.8.2,MIT exports-loader,0.6.4,MIT -express,4.14.1,MIT +express,4.15.3,MIT expression_parser,0.9.0,MIT extend,3.0.0,MIT extglob,0.3.2,MIT extlib,0.9.16,MIT -extract-zip,1.5.0,Simplified BSD extsprintf,1.0.2,MIT faraday,0.11.0,MIT faraday_middleware,0.11.0.1,MIT @@ -378,7 +382,6 @@ fast-levenshtein,2.0.6,MIT fast_gettext,1.4.0,"MIT,ruby" fastparse,1.1.1,MIT faye-websocket,0.7.3,MIT -fd-slicer,1.0.1,MIT ffi,1.9.10,BSD figures,1.7.0,MIT file-entry-cache,2.0.0,MIT @@ -387,13 +390,16 @@ filename-regex,2.0.0,MIT fileset,2.0.3,MIT filesize,3.3.0,New BSD fill-range,2.2.3,MIT -finalhandler,0.5.1,MIT +finalhandler,1.0.3,MIT find-cache-dir,0.1.1,MIT find-root,0.1.2,MIT find-up,2.1.0,MIT flat-cache,1.2.2,MIT flatten,1.0.2,MIT +flipper,0.10.2,MIT +flipper-active_record,0.10.2,MIT flowdock,0.7.1,MIT +fog-aliyun,0.1.0,MIT fog-aws,0.13.0,MIT fog-core,1.44.1,MIT fog-google,0.5.0,MIT @@ -409,9 +415,8 @@ forever-agent,0.6.1,Apache 2.0 form-data,2.1.2,MIT formatador,0.2.5,MIT forwarded,0.1.0,MIT -fresh,0.3.0,MIT +fresh,0.5.0,MIT from,0.1.7,MIT -fs-extra,1.0.0,MIT fs.realpath,1.0.0,ISC fsevents,,unknown fstream,1.0.10,ISC @@ -427,7 +432,7 @@ get_process_mem,0.2.0,MIT getpass,0.1.6,MIT gettext_i18n_rails,1.8.0,MIT gettext_i18n_rails_js,1.2.0,MIT -gitaly,0.6.0,MIT +gitaly,0.8.0,MIT github-linguist,4.7.6,MIT github-markup,1.4.0,MIT gitlab-flowdock-git-hook,1.0.1,MIT @@ -467,7 +472,6 @@ has-flag,1.0.0,MIT has-unicode,2.0.1,ISC hash-sum,1.0.2,MIT hash.js,1.0.3,MIT -hasha,2.2.0,MIT hashie,3.5.5,MIT hashie-forbidden_attributes,0.1.1,MIT hawk,3.1.3,New BSD @@ -487,7 +491,7 @@ htmlparser2,3.9.2,MIT http,0.9.8,MIT http-cookie,1.0.3,MIT http-deceiver,1.2.7,MIT -http-errors,1.5.1,MIT +http-errors,1.6.1,MIT http-form_data,1.0.1,MIT http-proxy,1.16.2,MIT http-proxy-middleware,0.17.4,MIT @@ -516,7 +520,7 @@ inquirer,0.12.0,MIT interpret,1.0.1,MIT invariant,2.2.2,New BSD invert-kv,1.0.0,MIT -ipaddr.js,1.2.0,MIT +ipaddr.js,1.3.0,MIT ipaddress,0.8.3,MIT is-absolute,0.2.6,MIT is-absolute-url,2.1.0,MIT @@ -563,7 +567,7 @@ istanbul-lib-instrument,1.4.2,New BSD istanbul-lib-report,1.0.0-alpha.3,New BSD istanbul-lib-source-maps,1.1.0,New BSD istanbul-reports,1.0.1,New BSD -jasmine-core,2.5.2,MIT +jasmine-core,2.6.3,MIT jasmine-jquery,2.1.1,MIT jed,1.1.1,MIT jira-ruby,1.1.2,MIT @@ -587,7 +591,6 @@ json-stable-stringify,1.0.1,MIT json-stringify-safe,5.0.1,ISC json3,3.3.2,MIT json5,0.5.1,MIT -jsonfile,2.4.0,MIT jsonify,0.0.0,Public Domain jsonpointer,4.0.1,MIT jsprim,1.3.1,MIT @@ -595,17 +598,14 @@ jszip,3.1.3,(MIT OR GPL-3.0) jszip-utils,0.0.2,MIT or GPLv3 jwt,1.5.6,MIT kaminari,0.17.0,MIT -karma,1.4.1,MIT +karma,1.7.0,MIT karma-coverage-istanbul-reporter,0.2.0,MIT karma-jasmine,1.1.0,MIT karma-mocha-reporter,2.2.2,MIT -karma-phantomjs-launcher,1.0.2,MIT karma-sourcemap-loader,0.3.7,MIT karma-webpack,2.0.2,MIT -kew,0.7.0,Apache 2.0 kgio,2.10.0,LGPL-2.1+ kind-of,3.1.0,MIT -klaw,1.3.1,MIT kubeclient,2.2.0,MIT latest-version,1.0.1,MIT launchy,2.4.3,ISC @@ -667,7 +667,7 @@ methods,1.1.2,MIT micromatch,2.3.11,MIT miller-rabin,4.0.0,MIT mime,1.3.4,MIT -mime-db,1.26.0,MIT +mime-db,1.27.0,MIT mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0" mimemagic,0.3.0,MIT mini_portile2,2.1.0,MIT @@ -675,16 +675,20 @@ minimalistic-assert,1.0.0,ISC minimatch,3.0.3,ISC minimist,0.0.8,MIT mkdirp,0.5.1,MIT +mmap2,2.2.6,ruby moment,2.17.1,MIT mousetrap,1.4.6,Apache 2.0 mousetrap-rails,1.4.6,"MIT,Apache" ms,0.7.2,MIT +msgpack,1.1.0,Apache 2.0 multi_json,1.12.1,MIT multi_xml,0.6.0,MIT multipart-post,2.0.0,MIT mustermann,0.4.0,MIT mustermann-grape,0.4.0,MIT mute-stream,0.0.5,ISC +mysql2,0.3.20,MIT +name-all-modules-plugin,1.0.1,MIT nan,2.5.1,MIT natural-compare,1.4.0,MIT negotiator,0.6.1,MIT @@ -774,9 +778,16 @@ path-type,1.1.0,MIT pause-stream,0.0.11,"MIT,Apache2" pbkdf2,3.0.9,MIT pdfjs-dist,1.8.252,Apache 2.0 -pend,1.2.0,MIT +peek,1.0.1,MIT +peek-gc,0.0.2,MIT +peek-host,1.0.0,MIT +peek-mysql2,1.1.0,MIT +peek-performance_bar,1.2.1,MIT +peek-pg,1.3.0,MIT +peek-rblineprof,0.2.0,MIT +peek-redis,1.2.0,MIT +peek-sidekiq,1.0.3,MIT pg,0.18.4,"BSD,ruby,GPL" -phantomjs-prebuilt,2.1.14,Apache 2.0 pify,2.3.0,MIT pikaday,1.5.1,"BSD,MIT" pinkie,2.0.4,MIT @@ -833,8 +844,9 @@ private,0.1.7,MIT process,0.11.9,MIT process-nextick-args,1.0.7,MIT progress,1.1.8,MIT +prometheus-client-mmap,0.7.0.beta5,Apache 2.0 proto-list,1.2.4,ISC -proxy-addr,1.1.3,MIT +proxy-addr,1.1.4,MIT prr,0.0.0,MIT ps-tree,1.1.0,MIT pseudomap,1.0.2,ISC @@ -843,7 +855,7 @@ punycode,1.4.1,MIT pyu-ruby-sasl,0.0.3.3,MIT q,1.5.0,MIT qjobs,1.1.5,MIT -qs,6.2.0,New BSD +qs,6.3.0,New BSD query-string,4.3.2,MIT querystring,0.2.0,MIT querystring-es3,0.2.1,MIT @@ -868,7 +880,7 @@ randomatic,1.1.6,MIT randombytes,2.0.3,MIT range-parser,1.2.0,MIT raphael,2.2.7,MIT -raven-js,3.15.0,Simplified BSD +raven-js,3.14.0,Simplified BSD raw-body,2.2.0,MIT raw-loader,0.5.1,MIT rc,1.1.6,(BSD-2-Clause OR MIT OR Apache-2.0) @@ -906,7 +918,6 @@ repeat-element,1.1.2,MIT repeat-string,1.6.1,MIT repeating,2.0.1,MIT request,2.79.0,Apache 2.0 -request-progress,2.0.1,MIT request_store,1.3.1,MIT require-directory,2.1.1,MIT require-from-string,1.2.1,MIT @@ -924,7 +935,7 @@ rimraf,2.5.4,ISC rinku,2.0.0,ISC ripemd160,1.0.1,New BSD rotp,2.1.2,MIT -rouge,2.0.7,MIT +rouge,2.1.0,MIT rqrcode,0.7.0,MIT rqrcode-rails3,0.1.7,MIT ruby-fogbugz,0.2.1,MIT @@ -933,7 +944,7 @@ ruby-saml,1.4.1,MIT ruby_parser,3.8.4,MIT rubyntlm,0.5.2,MIT rubypants,0.2.0,BSD -rufus-scheduler,3.1.10,MIT +rufus-scheduler,3.4.0,MIT rugged,0.25.1.1,MIT run-async,0.1.0,MIT rx-lite,3.1.2,Apache 2.0 @@ -952,20 +963,20 @@ select2,3.5.2-browserify,unknown select2-rails,3.5.9.3,MIT semver,5.3.0,ISC semver-diff,2.1.0,MIT -send,0.14.2,MIT +send,0.15.3,MIT sentry-raven,2.4.0,Apache 2.0 serve-index,1.8.0,MIT -serve-static,1.11.2,MIT +serve-static,1.12.3,MIT set-blocking,2.0.0,ISC set-immediate-shim,1.0.1,MIT setimmediate,1.0.5,MIT -setprototypeof,1.0.2,ISC +setprototypeof,1.0.3,ISC settingslogic,2.0.9,MIT sexp_processor,4.8.0,MIT sha.js,2.4.8,MIT shelljs,0.7.6,New BSD sidekiq,5.0.0,LGPL -sidekiq-cron,0.4.4,MIT +sidekiq-cron,0.6.0,MIT sidekiq-limit_fetch,3.4.0,MIT sigmund,1.0.1,ISC signal-exit,3.0.2,ISC @@ -975,9 +986,9 @@ slash,1.0.0,MIT slice-ansi,0.0.4,MIT slide,1.1.6,ISC sntp,1.0.9,BSD -socket.io,1.7.2,MIT +socket.io,1.7.3,MIT socket.io-adapter,0.5.0,MIT -socket.io-client,1.7.2,MIT +socket.io-client,1.7.3,MIT socket.io-parser,2.3.1,MIT sockjs,0.3.18,MIT sockjs-client,1.0.1,MIT @@ -1030,7 +1041,6 @@ thread_safe,0.3.6,Apache 2.0 three,0.84.0,MIT three-orbit-controls,82.1.0,MIT three-stl-loader,1.0.4,MIT -throttleit,1.0.0,MIT through,2.3.8,MIT tilt,2.0.6,MIT timeago.js,2.0.5,MIT @@ -1038,7 +1048,7 @@ timed-out,2.0.0,MIT timers-browserify,2.0.2,MIT timfel-krb5-auth,0.8.3,LGPL tiny-emitter,1.1.0,MIT -tmp,0.0.28,MIT +tmp,0.0.31,MIT to-array,0.1.4,MIT to-arraybuffer,1.0.1,MIT to-fast-properties,1.0.2,MIT @@ -1054,15 +1064,15 @@ tty-browserify,0.0.0,MIT tunnel-agent,0.4.3,Apache 2.0 tweetnacl,0.14.5,Unlicense type-check,0.3.2,MIT -type-is,1.6.14,MIT +type-is,1.6.15,MIT typedarray,0.0.6,MIT tzinfo,1.2.2,MIT u2f,0.2.1,MIT uglifier,2.7.2,MIT -uglify-js,2.8.21,Simplified BSD +uglify-js,2.8.27,Simplified BSD uglify-to-browserify,1.0.2,MIT uid-number,0.0.6,ISC -ultron,1.0.2,MIT +ultron,1.1.0,MIT unc-path-regex,0.1.2,MIT undefsafe,0.0.3,MIT / http://rem.mit-license.org underscore,1.8.3,MIT @@ -1081,14 +1091,14 @@ url-loader,0.5.8,MIT url-parse,1.0.5,MIT url_safe_base64,0.2.2,MIT user-home,2.0.0,MIT -useragent,2.1.12,MIT +useragent,2.1.13,MIT util,0.10.3,MIT util-deprecate,1.0.2,MIT utils-merge,1.0.0,MIT uuid,3.0.1,MIT validate-npm-package-license,3.0.1,Apache 2.0 validates_hostname,1.0.6,MIT -vary,1.1.0,MIT +vary,1.1.1,MIT vendors,1.0.1,MIT verror,1.3.6,MIT version_sorter,2.1.0,MIT @@ -1107,8 +1117,8 @@ vue-template-es2015-compiler,1.5.1,MIT warden,1.2.6,MIT watchpack,1.3.1,MIT wbuf,1.7.2,MIT -webpack,2.3.3,MIT -webpack-bundle-analyzer,2.3.0,MIT +webpack,2.6.1,MIT +webpack-bundle-analyzer,2.8.2,MIT webpack-dev-middleware,1.10.0,MIT webpack-dev-server,2.4.2,MIT webpack-rails,0.9.10,MIT @@ -1127,14 +1137,14 @@ wrap-ansi,2.1.0,MIT wrappy,1.0.2,ISC write,0.2.1,MIT write-file-atomic,1.3.1,ISC -ws,1.1.1,MIT +ws,2.3.1,MIT wtf-8,1.0.0,MIT xdg-basedir,2.0.0,MIT +xml-simple,1.1.5,ruby xmlhttprequest-ssl,1.5.3,MIT xtend,4.0.1,MIT y18n,3.2.1,ISC yallist,2.1.2,ISC yargs,3.10.0,MIT yargs-parser,4.2.1,ISC -yauzl,2.4.1,MIT yeast,0.1.2,MIT diff --git a/yarn.lock b/yarn.lock index 1db64aead8d..b902d5235d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -882,20 +882,20 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -body-parser@^1.12.4: - version "1.16.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b" +body-parser@^1.16.1: + version "1.17.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" dependencies: bytes "2.4.0" content-type "~1.0.2" - debug "2.6.0" + debug "2.6.7" depd "~1.1.0" - http-errors "~1.5.1" + http-errors "~1.6.1" iconv-lite "0.4.15" on-finished "~2.3.0" - qs "6.2.1" + qs "6.4.0" raw-body "~2.2.0" - type-is "~1.6.14" + type-is "~1.6.15" boom@2.x.x: version "2.10.1" @@ -1265,14 +1265,6 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611" - dependencies: - inherits "~2.0.1" - readable-stream "~2.0.0" - typedarray "~0.0.5" - concat-stream@^1.4.6: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" @@ -1305,12 +1297,12 @@ connect-history-api-fallback@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169" -connect@^3.3.5: - version "3.5.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.5.0.tgz#b357525a0b4c1f50599cd983e1d9efeea9677198" +connect@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.2.tgz#694e8d20681bfe490282c8ab886be98f09f42fe7" dependencies: - debug "~2.2.0" - finalhandler "0.5.0" + debug "2.6.7" + finalhandler "1.0.3" parseurl "~1.3.1" utils-merge "1.0.0" @@ -1538,10 +1530,6 @@ de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" -debug@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" - debug@2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" @@ -1554,18 +1542,18 @@ debug@2.3.3: dependencies: ms "0.7.2" -debug@2.6.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" - dependencies: - ms "0.7.2" - debug@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" dependencies: ms "2.0.0" +debug@^2.1.0, debug@^2.1.1, debug@^2.2.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" + dependencies: + ms "0.7.2" + decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -1778,9 +1766,9 @@ end-of-stream@1.0.0: dependencies: once "~1.3.0" -engine.io-client@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.2.tgz#c38767547f2a7d184f5752f6f0ad501006703766" +engine.io-client@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab" dependencies: component-emitter "1.2.1" component-inherit "0.0.3" @@ -1791,7 +1779,7 @@ engine.io-client@1.8.2: parsejson "0.0.3" parseqs "0.0.5" parseuri "0.0.5" - ws "1.1.1" + ws "1.1.2" xmlhttprequest-ssl "1.5.3" yeast "0.1.2" @@ -1806,16 +1794,16 @@ engine.io-parser@1.3.2: has-binary "0.1.7" wtf-8 "1.0.0" -engine.io@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.2.tgz#6b59be730b348c0125b0a4589de1c355abcf7a7e" +engine.io@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4" dependencies: accepts "1.3.3" base64id "1.0.0" cookie "0.3.1" debug "2.3.3" engine.io-parser "1.3.2" - ws "1.1.1" + ws "1.1.2" enhanced-resolve@^3.0.0: version "3.1.0" @@ -1884,10 +1872,6 @@ es6-promise@^3.0.2, es6-promise@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" -es6-promise@~4.0.3: - version "4.0.5" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" - es6-set@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" @@ -2219,15 +2203,6 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extract-zip@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.5.0.tgz#92ccf6d81ef70a9fa4c1747114ccef6d8688a6c4" - dependencies: - concat-stream "1.5.0" - debug "0.7.4" - mkdirp "0.5.0" - yauzl "2.4.1" - extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" @@ -2258,12 +2233,6 @@ faye-websocket@~0.7.3: dependencies: websocket-driver ">=0.3.6" -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - dependencies: - pend "~1.2.0" - figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2313,17 +2282,7 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -finalhandler@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" - dependencies: - debug "~2.2.0" - escape-html "~1.0.3" - on-finished "~2.3.0" - statuses "~1.3.0" - unpipe "~1.0.0" - -finalhandler@~1.0.3: +finalhandler@1.0.3, finalhandler@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" dependencies: @@ -2407,13 +2366,11 @@ from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" -fs-extra@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" +fs-access@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" + null-check "^1.0.0" fs.realpath@^1.0.0: version "1.0.0" @@ -2551,7 +2508,7 @@ got@^3.2.0: read-all-stream "^3.0.0" timed-out "^2.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -2628,13 +2585,6 @@ hash.js@^1.0.0: dependencies: inherits "^2.0.1" -hasha@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" - dependencies: - is-stream "^1.0.1" - pinkie-promise "^2.0.0" - hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -2695,7 +2645,7 @@ http-deceiver@^1.2.4: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" -http-errors@~1.5.0, http-errors@~1.5.1: +http-errors@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" dependencies: @@ -2987,7 +2937,7 @@ is-resolvable@^1.0.0: dependencies: tryit "^1.0.1" -is-stream@^1.0.0, is-stream@^1.0.1: +is-stream@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3124,9 +3074,9 @@ istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" -jasmine-core@^2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.5.2.tgz#6f61bd79061e27f43e6f9355e44b3c6cab6ff297" +jasmine-core@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.6.3.tgz#45072950e4a42b1e322fe55c001100a465d77815" jasmine-jquery@^2.1.1: version "2.1.1" @@ -3225,12 +3175,6 @@ json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - optionalDependencies: - graceful-fs "^4.1.6" - jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -3261,6 +3205,13 @@ jszip@^3.1.3: pako "~1.0.2" readable-stream "~2.0.6" +karma-chrome-launcher@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.1.1.tgz#216879c68ac04d8d5140e99619ba04b59afd46cf" + dependencies: + fs-access "^1.0.0" + which "^1.2.1" + karma-coverage-istanbul-reporter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-0.2.0.tgz#5766263338adeb0026f7e4ac7a89a5f056c5642c" @@ -3277,13 +3228,6 @@ karma-mocha-reporter@^2.2.2: dependencies: chalk "1.1.3" -karma-phantomjs-launcher@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1" - dependencies: - lodash "^4.0.1" - phantomjs-prebuilt "^2.1.7" - karma-sourcemap-loader@^0.3.7: version "0.3.7" resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8" @@ -3300,16 +3244,16 @@ karma-webpack@^2.0.2: source-map "^0.1.41" webpack-dev-middleware "^1.0.11" -karma@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/karma/-/karma-1.4.1.tgz#41981a71d54237606b0a3ea8c58c90773f41650e" +karma@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269" dependencies: bluebird "^3.3.0" - body-parser "^1.12.4" + body-parser "^1.16.1" chokidar "^1.4.1" colors "^1.1.0" combine-lists "^1.0.0" - connect "^3.3.5" + connect "^3.6.0" core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" @@ -3321,20 +3265,16 @@ karma@^1.4.1: lodash "^3.8.0" log4js "^0.6.31" mime "^1.3.4" - minimatch "^3.0.0" + minimatch "^3.0.2" optimist "^0.6.1" qjobs "^1.1.4" range-parser "^1.2.0" - rimraf "^2.3.3" + rimraf "^2.6.0" safe-buffer "^5.0.1" - socket.io "1.7.2" + socket.io "1.7.3" source-map "^0.5.3" - tmp "0.0.28" - useragent "^2.1.10" - -kew@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + tmp "0.0.31" + useragent "^2.1.12" kind-of@^3.0.2: version "3.1.0" @@ -3342,12 +3282,6 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.0.2" -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - optionalDependencies: - graceful-fs "^4.1.9" - latest-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" @@ -3556,7 +3490,7 @@ lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3704,12 +3638,6 @@ minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -mkdirp@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" - dependencies: - minimist "0.0.8" - mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -3907,6 +3835,10 @@ npmlog@^4.0.1: gauge "~2.7.1" set-blocking "~2.0.0" +null-check@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -4165,24 +4097,6 @@ pdfjs-dist@^1.8.252: node-ensure "^0.0.0" worker-loader "^0.8.0" -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - -phantomjs-prebuilt@^2.1.7: - version "2.1.14" - resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.14.tgz#d53d311fcfb7d1d08ddb24014558f1188c516da0" - dependencies: - es6-promise "~4.0.3" - extract-zip "~1.5.0" - fs-extra "~1.0.0" - hasha "~2.2.0" - kew "~0.7.0" - progress "~1.1.8" - request "~2.79.0" - request-progress "~2.0.1" - which "~1.2.10" - pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -4518,7 +4432,7 @@ process@^0.11.0, process@~0.11.0: version "0.11.9" resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" -progress@^1.1.8, progress@~1.1.8: +progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -4573,10 +4487,6 @@ qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" -qs@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" - qs@6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -4701,7 +4611,7 @@ readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2. string_decoder "~0.10.x" util-deprecate "~1.0.1" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" dependencies: @@ -4855,13 +4765,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-progress@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" - dependencies: - throttleit "^1.0.0" - -request@^2.79.0, request@~2.79.0: +request@^2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" dependencies: @@ -4934,7 +4838,13 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4: +rimraf@2, rimraf@^2.2.8, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -5094,15 +5004,15 @@ socket.io-adapter@0.5.0: debug "2.3.3" socket.io-parser "2.3.1" -socket.io-client@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.2.tgz#39fdb0c3dd450e321b7e40cfd83612ec533dd644" +socket.io-client@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377" dependencies: backo2 "1.0.2" component-bind "1.0.0" component-emitter "1.2.1" debug "2.3.3" - engine.io-client "1.8.2" + engine.io-client "1.8.3" has-binary "0.1.7" indexof "0.0.1" object-component "0.0.3" @@ -5119,16 +5029,16 @@ socket.io-parser@2.3.1: isarray "0.0.1" json3 "3.3.2" -socket.io@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.2.tgz#83bbbdf2e79263b378900da403e7843e05dc3b71" +socket.io@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b" dependencies: debug "2.3.3" - engine.io "1.8.2" + engine.io "1.8.3" has-binary "0.1.7" object-assign "4.1.0" socket.io-adapter "0.5.0" - socket.io-client "1.7.2" + socket.io-client "1.7.3" socket.io-parser "2.3.1" sockjs-client@1.0.1: @@ -5269,7 +5179,7 @@ stats-webpack-plugin@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/stats-webpack-plugin/-/stats-webpack-plugin-0.4.3.tgz#b2f618202f28dd04ab47d7ecf54ab846137b7aea" -"statuses@>= 1.3.1 < 2", statuses@~1.3.0, statuses@~1.3.1: +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -5449,10 +5359,6 @@ three@^0.84.0: version "0.84.0" resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918" -throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - through@2, through@^2.3.6, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -5481,9 +5387,9 @@ tiny-emitter@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb" -tmp@0.0.28, tmp@0.0.x: - version "0.0.28" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" +tmp@0.0.31, tmp@0.0.x: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" dependencies: os-tmpdir "~1.0.1" @@ -5541,14 +5447,14 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.14, type-is@~1.6.15: +type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: media-typer "0.3.0" mime-types "~2.1.15" -typedarray@^0.0.6, typedarray@~0.0.5: +typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5653,9 +5559,9 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@^2.1.10: - version "2.1.12" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.12.tgz#aa7da6cdc48bdc37ba86790871a7321d64edbaa2" +useragent@^2.1.12: + version "2.1.13" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.13.tgz#bba43e8aa24d5ceb83c2937473e102e21df74c10" dependencies: lru-cache "2.2.x" tmp "0.0.x" @@ -5883,7 +5789,7 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" -which@^1.1.1, which@~1.2.10: +which@^1.1.1, which@^1.2.1: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: @@ -5942,9 +5848,9 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" +ws@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f" dependencies: options ">=0.0.5" ultron "1.0.x" @@ -6015,12 +5921,6 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - dependencies: - fd-slicer "~1.0.1" - yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" |