diff options
844 files changed, 20533 insertions, 7962 deletions
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 76a95ad6e0a..5723b836c76 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,7 +63,7 @@ stages: .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql only: - /mysql/ - - /-stable$/ + - /-stable/ - master@gitlab-org/gitlab-ce - master@gitlab/gitlabhq - tags@gitlab-org/gitlab-ce @@ -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 @@ -450,6 +452,7 @@ karma: 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: @@ -471,8 +474,10 @@ codeclimate: services: - docker:dind script: + - docker pull stedolan/jq - 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 + - 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 > raw_codeclimate.json + - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json artifacts: paths: [codeclimate.json] @@ -547,3 +552,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/CHANGELOG.md b/CHANGELOG.md index af5f5809c41..f372cbf91e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,234 @@ 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) 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/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 @@ -2,6 +2,7 @@ source 'https://rubygems.org' gem 'rails', '4.2.8' gem 'rails-deprecated_sanitizer', '~> 1.0.3' +gem 'bootsnap', '~> 1.1' # Responders respond_to and respond_with gem 'responders', '~> 2.0' @@ -256,7 +257,7 @@ 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 @@ -354,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' diff --git a/Gemfile.lock b/Gemfile.lock index 4715363c5b8..f4ddd30da1b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,6 +83,8 @@ GEM bindata (2.3.5) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) + bootsnap (1.1.1) + msgpack (~> 1.0) bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) @@ -137,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) @@ -352,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) @@ -366,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) @@ -461,8 +463,9 @@ 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) multi_xml (0.6.0) multipart-post (2.0.0) @@ -589,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) @@ -887,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 @@ -926,6 +930,7 @@ DEPENDENCIES benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) binding_of_caller (~> 0.7.2) + bootsnap (~> 1.1) bootstrap-sass (~> 3.3.0) bootstrap_form (~> 2.7.0) brakeman (~> 3.6.0) @@ -1045,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) @@ -1117,7 +1122,7 @@ 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) 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/awards_handler.js b/app/assets/javascripts/awards_handler.js index ebe722061d7..c34d80f0601 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,11 +2,7 @@ /* 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'; @@ -17,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', @@ -30,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"> @@ -59,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> @@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) { export default class AwardsHandler { constructor() { 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'); @@ -81,8 +54,6 @@ export default class AwardsHandler { this.createEmojiMenu(); }); } - // Prebuild the categoryMap - categoryMap = categoryMap || buildCategoryMap(); }); this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { e.stopPropagation(); @@ -168,7 +139,7 @@ export default class AwardsHandler { this.isCreatingEmojiMenu = true; // Render the first category - categoryMap = categoryMap || buildCategoryMap(); + const categoryMap = Emoji.getEmojiCategoryMap(); const categoryNameKey = Object.keys(categoryMap)[0]; const emojisInCategory = categoryMap[categoryNameKey]; const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); @@ -208,7 +179,7 @@ export default class AwardsHandler { } 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 @@ -262,14 +233,8 @@ export default class AwardsHandler { return $menu.css(css); } - addAward( - votesBlock, - awardUrl, - emoji, - checkMutuality, - callback, - ) { - const normalizedEmoji = this.normalizeEmojiName(emoji); + 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); @@ -279,16 +244,12 @@ export default class AwardsHandler { $('.js-add-award.is-active').removeClass('is-active'); } - addAwardToEmojiBar( - votesBlock, - emoji, - checkForMutuality, - ) { + addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) { if (checkForMutuality || checkForMutuality === null) { this.checkMutuality(votesBlock, emoji); } this.addEmojiToFrequentlyUsedList(emoji); - const normalizedEmoji = this.normalizeEmojiName(emoji); + const normalizedEmoji = Emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); if ($emojiButton.length > 0) { if (this.isActive($emojiButton)) { @@ -413,7 +374,7 @@ export default class AwardsHandler { 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> `; @@ -478,12 +439,8 @@ export default class AwardsHandler { return $('body, html').animate(options, 200); } - normalizeEmojiName(emoji) { - return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji; - } - addEmojiToFrequentlyUsedList(emoji) { - if (isEmojiNameValid(emoji)) { + if (Emoji.isEmojiNameValid(emoji)) { this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); } @@ -493,7 +450,7 @@ export default class AwardsHandler { return this.frequentlyUsedEmojis || (() => { const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( - inputName => isEmojiNameValid(inputName), + inputName => Emoji.isEmojiNameValid(inputName), ); return this.frequentlyUsedEmojis; @@ -535,21 +492,11 @@ export default class AwardsHandler { } } - findMatchingEmojiElements(term) { - const safeTerm = term.toLowerCase(); - - const namesMatchingAlias = []; - Object.keys(emojiAliases).forEach((alias) => { - if (alias.indexOf(safeTerm) >= 0) { - namesMatchingAlias.push(emojiAliases[alias]); - } - }); - const $matchingElements = namesMatchingAlias.concat(safeTerm) - .reduce( - ($result, searchTerm) => - $result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)), - $([]), - ); + 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(); } 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/build.js b/app/assets/javascripts/build.js index c28f6e151a0..60103155ce0 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -85,9 +85,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); - - this.verifyTopPosition(); + }) + .then(() => this.verifyTopPosition()); } Build.prototype.canScroll = function () { @@ -176,7 +175,7 @@ window.Build = (function () { } if ($flashError.length) { - topPostion += $flashError.outerHeight(); + topPostion += $flashError.outerHeight() + prependTopDefault; } this.$buildTrace.css({ @@ -196,6 +195,7 @@ window.Build = (function () { }) .done((log) => { gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); + if (log.state) { this.state = log.state; } @@ -220,7 +220,11 @@ window.Build = (function () { } if (!log.complete) { - this.toggleScrollAnimation(true); + if (!this.hasBeenScrolled) { + this.toggleScrollAnimation(true); + } else { + this.toggleScrollAnimation(false); + } Build.timeout = setTimeout(() => { //eslint-disable-next-line @@ -229,7 +233,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); + }) + .then(() => this.verifyTopPosition()); }, 4000); } else { this.$buildRefreshAnimation.remove(); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 725ec7b9c70..1be9df19c81 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import './lib/utils/url_utility'; +import FilesCommentButton from './files_comment_button'; const UNFOLD_COUNT = 20; let isBound = false; @@ -8,8 +9,10 @@ let isBound = false; class Diff { constructor() { const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); - $diffFile.filesCommentButton(); + + FilesCommentButton.init($diffFile); $diffFile.each((index, file) => new gl.ImageFile(file)); diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 517bdb6be09..c37249c060a 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ const notesCount = this.notesCount; $(this.$el).closest('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0) + .toggleClass('no-comment-btn', notesCount > 0) .nextUntil('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0); + .toggleClass('no-comment-btn', notesCount > 0); }, toggleDiscussionsToggleState() { const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 88b4b567fa9..4247540de22 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; @@ -120,6 +121,9 @@ import initSettingsPanels from './settings_panels'; } switch (page) { + case 'profiles:preferences:show': + initExperimentalFlags(); + break; case 'sessions:new': new UsernameValidator(); new ActiveTabMemoizer(); @@ -205,8 +209,8 @@ import initSettingsPanels from './settings_panels'; new MilestoneSelect(); new gl.IssuableTemplateSelectors(); break; - case 'projects:merge_requests:new': - case 'projects:merge_requests:new_diffs': + case 'projects:merge_requests:creations:new': + case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); @@ -243,10 +247,6 @@ import initSettingsPanels from './settings_panels'; shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); break; - case "projects:merge_requests:diffs": - new gl.Diff(); - new ZenMode(); - break; case 'dashboard:activity': new gl.Activities(); break; @@ -315,7 +315,7 @@ import initSettingsPanels from './settings_panels'; new gl.Members(); new UsersSelect(); break; - case 'projects:members:show': + case 'projects:settings:members:show': new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new GroupsSelect(); new gl.MemberExpirationDate(); @@ -382,7 +382,7 @@ import initSettingsPanels from './settings_panels'; case 'search:show': new Search(); break; - case 'projects:repository:show': + case 'projects:settings:repository:show': // Initialize Protected Branch Settings new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); @@ -392,7 +392,7 @@ import initSettingsPanels from './settings_panels'; // Initialize expandable settings panels initSettingsPanels(); break; - case 'projects:ci_cd:show': + case 'projects:settings:ci_cd:show': new gl.ProjectVariables(); break; case 'ci:lints:create': 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_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/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/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 534e651b030..d02e4cd5876 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,150 +1,73 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ -/* global FilesCommentButton */ /* global notes */ -let $commentButtonTemplate; - -window.FilesCommentButton = (function() { - var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; - - COMMENT_BUTTON_CLASS = '.add-diff-note'; - - LINE_HOLDER_CLASS = '.line_holder'; - - LINE_NUMBER_CLASS = 'diff-line-num'; - - LINE_CONTENT_CLASS = 'line_content'; - - UNFOLDABLE_LINE_CLASS = 'js-unfold'; - - EMPTY_CELL_CLASS = 'empty-cell'; - - OLD_LINE_CLASS = 'old_line'; - - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - - TEXT_FILE_SELECTOR = '.text-file'; - - function FilesCommentButton(filesContainerElement) { - this.render = this.render.bind(this); - this.hideButton = this.hideButton.bind(this); - this.isParallelView = notes.isParallelView(); - filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) - .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); - } - - FilesCommentButton.prototype.render = function(e) { - var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; - $currentTarget = $(e.currentTarget); - - if ($currentTarget.hasClass('js-no-comment-btn')) return; - - lineContentElement = this.getLineContent($currentTarget); - buttonParentElement = this.getButtonParent($currentTarget); - - if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; - - $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); - buttonParentElement.addClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); - - if ($button.length) { - return; +/* Developer beware! Do not add logic to showButton or hideButton + * that will force a reflow. Doing so will create a signficant performance + * bottleneck for pages with large diffs. For a comprehensive list of what + * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a + */ + +const LINE_NUMBER_CLASS = 'diff-line-num'; +const UNFOLDABLE_LINE_CLASS = 'js-unfold'; +const NO_COMMENT_CLASS = 'no-comment-btn'; +const EMPTY_CELL_CLASS = 'empty-cell'; +const OLD_LINE_CLASS = 'old_line'; +const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`; +const DIFF_CONTAINER_SELECTOR = '.files'; +const DIFF_EXPANDED_CLASS = 'diff-expanded'; + +export default { + init($diffFile) { + /* Caching is used only when the following members are *true*. This is because there are likely to be + * differently configured versions of diffs in the same session. However if these values are true, they + * will be true in all cases */ + + if (!this.userCanCreateNote) { + // data-can-create-note is an empty string when true, otherwise undefined + this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; } - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - discussionID: lineContentElement.attr('data-discussion-id'), - lineType: lineContentElement.attr('data-line-type'), - - noteableType: textFileElement.attr('data-noteable-type'), - noteableID: textFileElement.attr('data-noteable-id'), - commitID: textFileElement.attr('data-commit-id'), - noteType: lineContentElement.attr('data-note-type'), - - // LegacyDiffNote - lineCode: lineContentElement.attr('data-line-code'), - - // DiffNote - position: lineContentElement.attr('data-position') - })); - }; - - FilesCommentButton.prototype.hideButton = function(e) { - var $currentTarget = $(e.currentTarget); - var buttonParentElement = this.getButtonParent($currentTarget); - - buttonParentElement.removeClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); - }; - - FilesCommentButton.prototype.buildButton = function(buttonAttributes) { - return $commentButtonTemplate.clone().attr({ - 'data-discussion-id': buttonAttributes.discussionID, - 'data-line-type': buttonAttributes.lineType, - - 'data-noteable-type': buttonAttributes.noteableType, - 'data-noteable-id': buttonAttributes.noteableID, - 'data-commit-id': buttonAttributes.commitID, - 'data-note-type': buttonAttributes.noteType, - - // LegacyDiffNote - 'data-line-code': buttonAttributes.lineCode, - - // DiffNote - 'data-position': buttonAttributes.position - }); - }; - - FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return hoveredElement.closest(TEXT_FILE_SELECTOR); - }; - - FilesCommentButton.prototype.getLineContent = function(hoveredElement) { - if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { - return hoveredElement; - } - if (!this.isParallelView) { - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); - } else { - return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + if (typeof notes !== 'undefined' && !this.isParallelView) { + this.isParallelView = notes.isParallelView && notes.isParallelView(); } - }; - FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (!this.isParallelView) { - if (hoveredElement.hasClass(OLD_LINE_CLASS)) { - return hoveredElement; - } - return hoveredElement.parent().find("." + OLD_LINE_CLASS); - } else { - if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { - return hoveredElement; - } - return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + if (this.userCanCreateNote) { + $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) + .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); } - }; + }, - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); - }; + showButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== ''; - }; + if (!this.validateButtonParent(buttonParentElement)) return; - return FilesCommentButton; -})(); + buttonParentElement.classList.add('is-over'); + buttonParentElement.nextElementSibling.classList.add('is-over'); + }, -$.fn.filesCommentButton = function() { - $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); + hideButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - if (!(this && (this.parent().data('can-create-note') != null))) { - return; - } - return this.each(function() { - if (!$.data(this, 'filesCommentButton')) { - return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); + buttonParentElement.classList.remove('is-over'); + buttonParentElement.nextElementSibling.classList.remove('is-over'); + }, + + getButtonParent(hoveredElement, isParallelView) { + if (isParallelView) { + if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) { + return hoveredElement.previousElementSibling; + } + } else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) { + return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`); } - }); + return hoveredElement; + }, + + validateButtonParent(buttonParentElement) { + return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && + !buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) && + !buttonParentElement.classList.contains(NO_COMMENT_CLASS) && + !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS); + }, }; diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 65c1b2050ac..19fed771197 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -2,6 +2,7 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter'; import './filtered_search_dropdown'; +import { addClassIfElementExists } from '../lib/utils/dom_utils'; class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, tokenKeys, filter) { @@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { } hideCurrentUser() { - const currenUserItem = this.dropdown.querySelector('.js-current-user'); - currenUserItem.classList.add('hidden'); + addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden'); } itemClicked(e) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 1425769d2de..7872e9e68ad 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesService from './services/recent_searches_service'; import eventHub from './event_hub'; +import { addClassIfElementExists } from '../lib/utils/dom_utils'; class FilteredSearchManager { constructor(page) { @@ -227,11 +228,7 @@ class FilteredSearchManager { } addInputContainerFocus() { - const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); - - if (inputContainer) { - inputContainer.classList.add('focus'); - } + addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus'); } removeInputContainerFocus(e) { diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 105762cb1ba..10a64f9032b 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, ''); @@ -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) => { @@ -398,6 +396,13 @@ class GfmAutoComplete { this.cachedData = {}; } + destroy() { + this.input.each((i, input) => { + const $input = $(input); + $input.atwho('destroy'); + }); + } + static isLoading(data) { let dataToInspect = data; if (data && data.length > 0) { diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index dc9f114af99..4e8141b2956 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) { GLForm.prototype.destroy = function() { // Clean form listeners this.clearEventListeners(); + if (this.autoComplete) { + this.autoComplete.destroy(); + } return this.form.data('gl-form', null); }; @@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() { this.form.addClass('gfm-form'); // remove notify commit author checkbox for non-commit notes gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); - new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), { + this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + this.autoComplete.setup(this.form.find('.js-gfm-input'), { emojis: true, members: this.enableGFM, issues: this.enableGFM, diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 54650d2f184..27b1b814f9a 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -47,7 +47,8 @@ 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/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index 4223a8fea49..d0145fed396 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -39,6 +39,17 @@ runnerId() { return `#${this.job.runner.id}`; }, + renderBlock() { + return this.job.merge_request || + this.job.duration || + this.job.finished_data || + this.job.erased_at || + this.job.queued || + this.job.runner || + this.job.coverage || + this.job.tags.length || + this.job.cancel_path; + }, }, }; </script> @@ -63,7 +74,7 @@ Retry </a> </div> - <div class="block"> + <div :class="{block : renderBlock }"> <p class="build-detail-row js-job-mr" v-if="job.merge_request"> 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/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js new file mode 100644 index 00000000000..de65ea15a60 --- /dev/null +++ b/app/assets/javascripts/lib/utils/dom_utils.js @@ -0,0 +1,7 @@ +/* eslint-disable import/prefer-default-export */ + +export const addClassIfElementExists = (element, className) => { + if (element) { + element.classList.add(className); + } +}; 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 d634af959e5..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;"},"%{commit_author_link} committed %{commit_timeago}":[""],"About auto deploy":[""],"Active":[""],"Activity":[""],"Add Changelog":[""],"Add Contribution guide":[""],"Add License":[""],"Add an SSH key to your profile to pull or push via SSH.":[""],"Add new directory":[""],"Archived project! Repository is read-only":[""],"Are you sure you want to delete this pipeline schedule?":[""],"Attach a file by drag & drop or %{upload_link}":[""],"Branch":["",""],"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}":[""],"Branches":[""],"Browse files":[""],"ByAuthor|by":[""],"CI configuration":[""],"Cancel":[""],"ChangeTypeActionLabel|Pick into branch":[""],"ChangeTypeActionLabel|Revert in branch":[""],"ChangeTypeAction|Cherry-pick":[""],"Changelog":[""],"Charts":[""],"Cherry-pick this commit":[""],"Cherry-pick this merge request":[""],"CiStatusLabel|canceled":[""],"CiStatusLabel|created":[""],"CiStatusLabel|failed":[""],"CiStatusLabel|manual action":[""],"CiStatusLabel|passed":[""],"CiStatusLabel|passed with warnings":[""],"CiStatusLabel|pending":[""],"CiStatusLabel|skipped":[""],"CiStatusLabel|waiting for manual action":[""],"CiStatusText|blocked":[""],"CiStatusText|canceled":[""],"CiStatusText|created":[""],"CiStatusText|failed":[""],"CiStatusText|manual":[""],"CiStatusText|passed":[""],"CiStatusText|pending":[""],"CiStatusText|skipped":[""],"CiStatus|running":[""],"Commit":["",""],"Commit message":[""],"CommitBoxTitle|Commit":[""],"CommitMessage|Add %{file_name}":[""],"Commits":[""],"Commits|History":[""],"Committed by":[""],"Compare":[""],"Contribution guide":[""],"Contributors":[""],"Copy URL to clipboard":[""],"Copy commit SHA to clipboard":[""],"Create New Directory":[""],"Create directory":[""],"Create empty bare repository":[""],"Create merge request":[""],"Create new...":[""],"CreateNewFork|Fork":[""],"CreateTag|Tag":[""],"Cron Timezone":[""],"Cron syntax":[""],"Custom notification events":[""],"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}.":[""],"Cycle Analytics":[""],"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":[""],"Define a custom pattern with cron syntax":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Directory name":[""],"Don't show again":[""],"Download":[""],"Download tar":[""],"Download tar.bz2":[""],"Download tar.gz":[""],"Download zip":[""],"DownloadArtifacts|Download":[""],"DownloadCommit|Email Patches":[""],"DownloadCommit|Plain Diff":[""],"DownloadSource|Download":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Every day (at 4:00am)":[""],"Every month (on the 1st at 4:00am)":[""],"Every week (Sundays at 4:00am)":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Files":[""],"Find by path":[""],"Find file":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"Fork":["",""],"ForkedFromProjectPath|Forked from":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Go to your fork":[""],"GoToYourFork|Fork":[""],"Home":[""],"Housekeeping successfully started":[""],"Import repository":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"LFSStatus|Disabled":[""],"LFSStatus|Enabled":[""],"Last %d day":["",""],"Last Pipeline":[""],"Last Update":[""],"Last commit":[""],"Learn more in the":[""],"Learn more in the|pipeline schedules documentation":[""],"Leave group":[""],"Leave project":[""],"Limited to showing %d event at most":["",""],"Median":[""],"MissingSSHKeyWarningLink|add an SSH key":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"New branch":[""],"New directory":[""],"New file":[""],"New issue":[""],"New merge request":[""],"New schedule":[""],"New snippet":[""],"New tag":[""],"No repository":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"Notification events":[""],"NotificationEvent|Close issue":[""],"NotificationEvent|Close merge request":[""],"NotificationEvent|Failed pipeline":[""],"NotificationEvent|Merge merge request":[""],"NotificationEvent|New issue":[""],"NotificationEvent|New merge request":[""],"NotificationEvent|New note":[""],"NotificationEvent|Reassign issue":[""],"NotificationEvent|Reassign merge request":[""],"NotificationEvent|Reopen issue":[""],"NotificationEvent|Successful pipeline":[""],"NotificationLevel|Custom":[""],"NotificationLevel|Disabled":[""],"NotificationLevel|Global":[""],"NotificationLevel|On mention":[""],"NotificationLevel|Participate":[""],"NotificationLevel|Watch":[""],"OfSearchInADropdown|Filter":[""],"OpenedNDaysAgo|Opened":[""],"Options":[""],"Owner":[""],"Pipeline":[""],"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":[""],"PipelineSheduleIntervalPattern|Custom":[""],"Pipeline|with stage":[""],"Pipeline|with stages":[""],"Project '%{project_name}' queued for deletion.":[""],"Project '%{project_name}' was successfully created.":[""],"Project '%{project_name}' was successfully updated.":[""],"Project '%{project_name}' will be deleted.":[""],"Project access must be granted explicitly to each user.":[""],"Project export could not be deleted.":[""],"Project export has been deleted.":[""],"Project export link has expired. Please generate a new export from your project settings.":[""],"Project export started. A download link will be sent by email.":[""],"Project home":[""],"ProjectFeature|Disabled":[""],"ProjectFeature|Everyone with access":[""],"ProjectFeature|Only team members":[""],"ProjectFileTree|Name":[""],"ProjectLastActivity|Never":[""],"ProjectLifecycle|Stage":[""],"ProjectNetworkGraph|Graph":[""],"Read more":[""],"Readme":[""],"RefSwitcher|Branches":[""],"RefSwitcher|Tags":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Remind later":[""],"Remove project":[""],"Request Access":[""],"Revert this commit":[""],"Revert this merge request":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Scheduling Pipelines":[""],"Search branches and tags":[""],"Select Archive Format":[""],"Select a timezone":[""],"Select target branch":[""],"Set a password on your account to pull or push via %{protocol}":[""],"Set up CI":[""],"Set up Koding":[""],"Set up auto deploy":[""],"SetPasswordToCloneLink|set a password":[""],"Showing %d event":["",""],"Source code":[""],"StarProject|Star":[""],"Start a %{new_merge_request} with these changes":[""],"Start a <strong>new merge request</strong> with these changes":[""],"Switch branch/tag":[""],"Tag":["",""],"Tags":[""],"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 fork relationship has been removed.":[""],"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 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 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 project can be accessed by any logged in user.":[""],"The project can be accessed without any authentication.":[""],"The repository for this project does not exist.":[""],"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.":[""],"This means you can not push code until you create an empty repository or import existing one.":[""],"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":[""],"Timeago|%s days ago":[""],"Timeago|%s days remaining":[""],"Timeago|%s hours remaining":[""],"Timeago|%s minutes ago":[""],"Timeago|%s minutes remaining":[""],"Timeago|%s months ago":[""],"Timeago|%s months remaining":[""],"Timeago|%s seconds remaining":[""],"Timeago|%s weeks ago":[""],"Timeago|%s weeks remaining":[""],"Timeago|%s years ago":[""],"Timeago|%s years remaining":[""],"Timeago|1 day remaining":[""],"Timeago|1 hour remaining":[""],"Timeago|1 minute remaining":[""],"Timeago|1 month remaining":[""],"Timeago|1 week remaining":[""],"Timeago|1 year remaining":[""],"Timeago|Past due":[""],"Timeago|a day ago":[""],"Timeago|a month ago":[""],"Timeago|a week ago":[""],"Timeago|a while":[""],"Timeago|a year ago":[""],"Timeago|about %s hours ago":[""],"Timeago|about a minute ago":[""],"Timeago|about an hour ago":[""],"Timeago|in %s days":[""],"Timeago|in %s hours":[""],"Timeago|in %s minutes":[""],"Timeago|in %s months":[""],"Timeago|in %s seconds":[""],"Timeago|in %s weeks":[""],"Timeago|in %s years":[""],"Timeago|in 1 day":[""],"Timeago|in 1 hour":[""],"Timeago|in 1 minute":[""],"Timeago|in 1 month":[""],"Timeago|in 1 week":[""],"Timeago|in 1 year":[""],"Timeago|less than a minute ago":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Unstar":[""],"Upload New File":[""],"Upload file":[""],"Use your global notification setting":[""],"VisibilityLevel|Internal":[""],"VisibilityLevel|Private":[""],"VisibilityLevel|Public":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"Withdraw Access Request":[""],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":[""],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":[""],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":[""],"You can only add files when you are on a branch":[""],"You must sign in to star a project":[""],"You need permission.":[""],"You will not get any notifications via email":[""],"You will only receive notifications for the events you choose":[""],"You will only receive notifications for threads you have participated in":[""],"You will receive notifications for any activity":[""],"You will receive notifications only for comments in which you were @mentioned":[""],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":[""],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":[""],"Your name":[""],"day":["",""],"new merge request":[""],"notification emails":[""],"parent":["",""]}}};
\ 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 8d951928849..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-21 12:09-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;"},"%d additional commit has been omitted to prevent performance issues.":["%d cambio adicional ha sido omitido para evitar problemas de rendimiento.","%d cambios adicionales han sido omitidos para evitar problemas de rendimiento."],"%d commit":["%d cambio","%d cambios"],"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} cambió %{commit_timeago}"],"About auto deploy":["Acerca del auto despliegue"],"Active":["Activo"],"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 solo lectura"],"Are you sure you want to delete this pipeline schedule?":["¿Estás seguro que deseas eliminar esta programación del pipeline?"],"Attach a file by drag & drop or %{upload_link}":["Adjunte un archivo arrastrando & soltando o %{upload_link}"],"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}"],"BranchSwitcherPlaceholder|Search branches":["Buscar ramas"],"BranchSwitcherTitle|Switch branch":["Cambiar rama"],"Branches":["Ramas"],"Browse Directory":["Examinar directorio"],"Browse File":["Examinar archivo"],"Browse Files":["Examinar archivos"],"Browse files":["Examinar archivos"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Cancel":["Cancelar"],"ChangeTypeActionLabel|Pick into branch":["Escoger en la rama"],"ChangeTypeActionLabel|Revert in branch":["Revertir en la rama"],"ChangeTypeAction|Cherry-pick":["Cherry-pick"],"ChangeTypeAction|Revert":["Revertir"],"Changelog":["Changelog"],"Charts":["Gráficos"],"Cherry-pick this commit":["Escoger este cambio"],"Cherry-pick this merge request":["Escoger esta solicitud de fusión"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallido"],"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"],"Commit message":["Mensaje del cambio"],"CommitBoxTitle|Commit":["Cambio"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits feed":["Feed de cambios"],"Commits|History":["Historial"],"Committed by":["Enviado por"],"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"],"Create new...":["Crear nuevo..."],"CreateNewFork|Fork":["Bifurcar"],"CreateTag|Tag":["Etiqueta"],"Cron Timezone":["Zona horaria del Cron"],"Cron syntax":["Sintaxis de Cron"],"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"],"Define a custom pattern with cron syntax":["Definir un patrón personalizado con la sintaxis de cron"],"Delete":["Eliminar"],"Deploy":["Despliegue","Despliegues"],"Description":["Descripción"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download":["Descargar"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadCommit|Email Patches":["Parches por correo electrónico"],"DownloadCommit|Plain Diff":["Diferencias en texto plano"],"DownloadSource|Download":["Descargar"],"Edit":["Editar"],"Edit Pipeline Schedule %{id}":["Editar Programación del Pipeline %{id}"],"Every day (at 4:00am)":["Todos los días (a las 4:00 am)"],"Every month (on the 1st at 4:00am)":["Todos los meses (el día 1 a las 4:00 am)"],"Every week (Sundays at 4:00am)":["Todas las semanas (domingos a las 4:00 am)"],"Failed to change the owner":["Error al cambiar el propietario"],"Failed to remove the pipeline schedule":["Error al eliminar la programación del pipeline"],"Files":["Archivos"],"Filter by commit message":["Filtrar por mensaje del cambio"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"Fork":["Bifurcación","Bifurcaciones"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"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"],"Interval Pattern":["Patrón de intervalo"],"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 Pipeline":["Último Pipeline"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Learn more in the":["Más información en la"],"Learn more in the|pipeline schedules documentation":["documentación sobre la programación de pipelines"],"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 Pipeline Schedule":["Nueva Programación del Pipeline"],"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 schedule":["Nueva programación"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"No schedules":["No hay programaciones"],"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"],"OfSearchInADropdown|Filter":["Filtrar"],"OpenedNDaysAgo|Opened":["Abierto"],"Options":["Opciones"],"Owner":["Propietario"],"Pipeline":["Pipeline"],"Pipeline Health":["Estado del Pipeline"],"Pipeline Schedule":["Programación del Pipeline"],"Pipeline Schedules":["Programaciones de los Pipelines"],"PipelineSchedules|Activated":["Activado"],"PipelineSchedules|Active":["Activos"],"PipelineSchedules|All":["Todos"],"PipelineSchedules|Inactive":["Inactivos"],"PipelineSchedules|Next Run":["Próxima Ejecución"],"PipelineSchedules|None":["Ninguno"],"PipelineSchedules|Provide a short description for this pipeline":["Proporcione una breve descripción para este pipeline"],"PipelineSchedules|Take ownership":["Tomar posesión"],"PipelineSchedules|Target":["Destino"],"PipelineSheduleIntervalPattern|Custom":["Personalizado"],"Pipeline|with stage":["con etapa"],"Pipeline|with stages":["con etapas"],"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":["Léeme"],"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"],"Revert this commit":["Revertir este cambio"],"Revert this merge request":["Revertir esta solicitud de fusión"],"Save pipeline schedule":["Guardar programación del pipeline"],"Schedule a new pipeline":["Programar un nuevo pipeline"],"Scheduling Pipelines":["Programación de Pipelines"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Select a timezone":["Selecciona una zona horaria"],"Select target branch":["Selecciona una rama de destino"],"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"],"Start a %{new_merge_request} with these changes":["Iniciar una %{new_merge_request} con estos cambios"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"Target Branch":["Rama de destino"],"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 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 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."],"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 un mes"],"Timeago|a week ago":["hace una semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace un 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"],"UploadLink|click to upload":["Hacer clic para subir"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"View open merge request":["Ver solicitud de fusión abierta"],"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":["Solo puedes agregar archivos cuando estás en una rama"],"You have reached your project limit":["Has alcanzado el límite de tu proyecto"],"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 por cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones solo 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"],"day":["día","días"],"new merge request":["nueva solicitud de fusión"],"notification emails":["correos electrónicos de notificación"],"parent":["padre","padres"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/fr/app.js b/app/assets/javascripts/locale/fr/app.js deleted file mode 100644 index f9904ea61ea..00000000000 --- a/app/assets/javascripts/locale/fr/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['fr'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-06-15 20:38+0000","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-14 04:21-0400","Last-Translator":"Dremor <egeorget@opmbx.org>","Language-Team":"French (https://www.transifex.com/gitlab-fr/teams/75145/fr/)","Language":"fr","Plural-Forms":"nplurals=2; plural=(n > 1);","X-Generator":"Zanata 3.9.6","lang":"fr","domain":"app","plural_forms":"nplurals=2; plural=(n > 1);"},"ByAuthor|by":["par"],"Commit":["Validation","Validations"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["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."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Incident"],"CycleAnalyticsStage|Plan":["Planification"],"CycleAnalyticsStage|Production":["Production"],"CycleAnalyticsStage|Review":["Examen"],"CycleAnalyticsStage|Staging":["Pré-production"],"CycleAnalyticsStage|Test":["Test"],"Deploy":["Déploiement","Déploiements"],"FirstPushedBy|First":["En premier"],"FirstPushedBy|pushed by":["poussé par"],"From issue creation until deploy to production":["Depuis la création de l'incident jusqu'au déploiement en production"],"From merge request merge until deploy to production":["Depuis la fusion de la demande de fusion jusqu'au déploiement en production"],"Introducing Cycle Analytics":["Introduction à l'analyseur de cycle"],"Last %d day":["Le dernier %d jour","Les derniers %d jours"],"Limited to showing %d event at most":["Limiter l'affichage au plus à %d évènement","Limiter l'affichage au plus à %d évènements"],"Median":["Médian"],"New Issue":["Nouvel incident","Nouveaux incidents"],"Not available":["Indisponible"],"Not enough data":["Données insuffisantes"],"OpenedNDaysAgo|Opened":["Ouvert"],"Pipeline Health":["Santé du Pipeline"],"ProjectLifecycle|Stage":["Étape"],"Read more":["Lire plus"],"Related Commits":["Validations liés"],"Related Deployed Jobs":["Tâches de déploiement liés"],"Related Issues":["Incidents liés"],"Related Jobs":["Tâches liées"],"Related Merge Requests":["Demandes de fusion liées"],"Related Merged Requests":["Demandes fusionnées liées"],"Showing %d event":["Affichage de %d évènement","Affichage de %d évènements"],"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.":["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."],"The collection of events added to the data gathered for that stage.":["L’ensemble d’évènements ajoutés aux données récupérées pour cette étape."],"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.":["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."],"The phase of the development lifecycle.":["Les étapes du cycle de développement."],"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.":["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."],"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.":["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."],"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.":["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."],"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.":["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."],"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.":["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."],"The time taken by each data entry gathered by that stage.":["Le temps pris par chaque entrée récoltée durant cette étape."],"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 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."],"Time before an issue gets scheduled":["Temps avant qu’un incident ne soit planifié"],"Time before an issue starts implementation":["Temps avant que résolution ne débute"],"Time between merge request creation and merge/close":["Temps entre la création d'une demande de fusion et sa fusion/clôture"],"Time until first merge request":["Temps jusqu’à la première demande de fusion"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Temps total"],"Total test time for all commits/merges":["Temps total de test pour toutes les validations/fusions"],"Want to see the data? Please ask an administrator for access.":["Vous voulez voir les données ? Merci de contacter un administrateur pour en obtenir l’accès."],"We don't have enough data to show this stage.":["Nous n'avons pas suffisamment de données pour afficher cette étape."],"You need permission.":["Vous avez besoin d’une autorisation."],"day":["jour","jours"],"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} a validé %{commit_timeago}"],"About auto deploy":["A propos de l'auto-déploiement"],"Active":["Actif"],"Activity":["Activité"],"Add Changelog":["Ajouter un journal des modifications"],"Add Contribution guide":["Ajouter un guide de contribution"],"Add License":["Ajouter une licence"],"Add an SSH key to your profile to pull or push via SSH.":["Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser par SSH."],"Add new directory":["Ajouter un nouveau dossier"],"Archived project! Repository is read-only":["Projet archivé ! Le dépôt est en lecture seule"],"Are you sure you want to delete this pipeline schedule?":["Êtes-vous sûr de vouloir supprimer ce pipeline programmé"],"Attach a file by drag & drop or %{upload_link}":["Attachez un fichier par glisser & déposer ou %{upload_link}"],"Branch":["Branche","Branches"],"#~ \"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, cho\"#~ \"ose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_do\"#~ \"c}\"":["#~ \"La branche <strong>%{branch_name}</strong> a été crée. Pour mettre en place le\"#~ \" déploiement automatisé, sélectionnez un modèle de fichier Yaml pour Gitlab CI\"#~ \", et validez les modifications. %{link_to_autodeploy_doc}\""],"Branches":["Branches"],"Browse files":["Parcourir les fichiers"],"CI configuration":["Configuration du CI"],"Cancel":["Annuler"],"ChangeTypeActionLabel|Pick into branch":["Sélectionner dans la branche"],"ChangeTypeActionLabel|Revert in branch":["Annuler dans la branche"],"ChangeTypeAction|Cherry-pick":["Sélectionner"],"ChangeType|commit":["validation"],"ChangeType|merge request":["demande de fusion"],"Changelog":["Journal des modifications"],"Charts":["Graphiques"],"Cherry-pick this commit":["Sélectionner cette validation"],"Cherry-pick this merge-request":["Sélectionner cette demande de fusion"],"CiStatusLabel|canceled":["annulé"],"CiStatusLabel|created":["créé"],"CiStatusLabel|failed":["échoué"],"CiStatusLabel|manual action":["action manuelle"],"CiStatusLabel|passed":["passé"],"CiStatusLabel|passed with warnings":["passé avec des avertissements"],"CiStatusLabel|pending":["en attente"],"CiStatusLabel|skipped":["ignoré"],"CiStatusLabel|waiting for manual action":["en attente d'action manuelle"],"CiStatusText|blocked":["bloqué"],"CiStatusText|canceled":["annulé "],"CiStatusText|created":["créé"],"CiStatusText|failed":["échoué"],"CiStatusText|manual":["manuel"],"CiStatusText|passed":["passé"],"CiStatusText|pending":["en attente"],"CiStatusText|skipped":["ignoré"],"CiStatus|running":["en cours"],"Commit message":["Message de validation"],"CommitMessage|Add %{file_name}":["Ajout de %{file_name}"],"Commits":["Validations"],"Commits|History":["Historique"],"Committed by":["Validé par"],"Compare":["Comparer"],"Contribution guide":["Guilde de contribution"],"Contributors":["Contributeurs"],"Copy URL to clipboard":["Copier l'URL dans le presse-papier"],"Copy commit SHA to clipboard":["Copier le SAH de la validation"],"Create New Directory":["Créer un nouveau dossier"],"Create directory":["Créer un dossier"],"Create empty bare repository":["Créer un dépôt vide"],"Create merge request":["Créer une demande de fusion"],"Create new...":["Créer nouveau..."],"CreateNewFork|Fork":["Fork"],"CreateTag|Tag":["Étiquette"],"Cron Timezone":["Fuseau horaire de Cron"],"Cron syntax":["Syntaxe CRON"],"Custom":["Personnalisé"],"Custom notification events":["Événements de notification personnalisés"],"#~ \"Custom notification levels are the same as participating levels. With custom n\"#~ \"otification levels you will also receive notifications for select events. To f\"#~ \"ind out more, check out %{notification_link}.\"":["#~ \"Le niveau de notification Personnalisé est similaire au niveau Participation. \"#~ \"Il permet cependant également de recevoir des notifications pour des événement\"#~ \"s sélectionnés. Pour plus d’information, vous pouvez consulter %{notification_\"#~ \"link}.\""],"Cycle Analytics":["Analyseur de cycle"],"Define a custom pattern with cron syntax":["Définir un schéma personnalisé avec une syntaxe CRON"],"Delete":["Supprimer"],"Description":["Description"],"Directory name":["Nom du dossier"],"Don't show again":["Ne plus montrer"],"Download":["Télécharger"],"Download tar":["Télécharger tar"],"Download tar.bz2":["Télécharger tar.bz2"],"Download tar.gz":["Télécharger tar.gz"],"Download zip":["Télécharger zip"],"DownloadArtifacts|Download":["Télécharger"],"DownloadCommit|Email Patches":["Patch email"],"DownloadCommit|Plain Diff":["Diff simple"],"DownloadSource|Download":["Télécharger"],"Edit":["Éditer"],"Edit Pipeline Schedule %{id}":["Éditer le pipeline programmé %{id}"],"Every day (at 4:00am)":["Chaque jour (à 4:00 du matin)"],"Every month (on the 1st at 4:00am)":["Chaque mois (le 1er à 4:00 du matin)"],"Every week (Sundays at 4:00am)":["Chaque semaine (Dimanche à 4:00 du matin)"],"Failed to change the owner":["Échec du changement de propriétaire"],"Failed to remove the pipeline schedule":["Échec de la suppression du pipeline programmé"],"Files":["Fichiers"],"Find by path":["Rechercher par chemin"],"Find file":["Rechercher un fichier"],"Fork":["Fork","Forks"],"ForkedFromProjectPath|Forked from":["Forké depuis"],"Go to your fork":["Aller à votre fork"],"GoToYourFork|Fork":["Fork"],"Home":["Accueil"],"Housekeeping successfully started":["Maintenance démarrée avec succès"],"Import repository":["Importer un dépôt"],"Interval Pattern":["Schéma d’intervalle"],"LFSStatus|Disabled":["Désactivé"],"LFSStatus|Enabled":["Activé"],"Last Pipeline":["Dernier pipeline"],"Last Update":["Dernière mise à jour"],"Last commit":["Dernière validation"],"Learn more in the":["En apprendre plus dans le"],"Leave group":["Quitter le groupe"],"Leave project":["Quitter le projet"],"MissingSSHKeyWarningLink|add an SSH key":["ajouter un clef SSH"],"New Pipeline Schedule":["Nouveau pipeline programmé"],"New branch":["Nouvelle branche"],"New directory":["Nouveau dossier"],"New file":["Nouveau Fichier"],"New issue":["Nouvel incident"],"New merge request":["Nouvelle demande de fusion"],"New schedule":["Nouveau programme"],"New snippet":["Nouvel extrait de code"],"New tag":["Nouvelle étiquette"],"No repository":["Pas de dépôt"],"No schedules":["Aucun programme"],"Notification events":["Événement de notifications"],"NotificationEvent|Close issue":["Clore l'incident"],"NotificationEvent|Close merge request":["Clore la demande de fusion"],"NotificationEvent|Failed pipeline":["Pipeline échoué"],"NotificationEvent|Merge merge request":["Fusionner le demande de fusion"],"NotificationEvent|New issue":["Nouvel incident"],"NotificationEvent|New merge request":["Nouvelle demande de fusion"],"NotificationEvent|New note":["Nouvelle note"],"NotificationEvent|Reassign issue":["Réassigner l'incident"],"NotificationEvent|Reassign merge request":["Réassigner la demande de fusion"],"NotificationEvent|Reopen issue":["Ré-ouvrir l'incident"],"NotificationEvent|Successful pipeline":["Pipeline réussi"],"NotificationLevel|Custom":["Personnalisé"],"NotificationLevel|Disabled":["Désactivé"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["En cas de mention"],"NotificationLevel|Participate":["Participation"],"NotificationLevel|Watch":["Surveillé"],"OfSearchInADropdown|Filter":["Filtre"],"Options":["Options"],"Owner":["Propriétaire"],"Pipeline":["Pipeline"],"Pipeline Schedule":["Programmation de pipeline"],"Pipeline Schedules":["Programmations de pipeline"],"PipelineSchedules|Activated":["Activé"],"PipelineSchedules|Active":["Active"],"PipelineSchedules|All":["Tous"],"PipelineSchedules|Inactive":["Inactive"],"PipelineSchedules|Next Run":["Prochaine exécution"],"PipelineSchedules|None":["Aucune"],"PipelineSchedules|Provide a short description for this pipeline":["Indiquez une courte description"],"PipelineSchedules|Take ownership":["S’approprier"],"PipelineSchedules|Target":["Cible"],"Project '%{project_name}' queued for deletion.":["Projet '%{project_name}' en attente de suppression."],"Project '%{project_name}' was successfully created.":["Projet '%{project_name}' créé avec succès."],"Project '%{project_name}' was successfully updated.":["Projet '%{project_name}' mis à jour avec succès."],"Project '%{project_name}' will be deleted.":["Projet '%{project_name}' sera supprimé."],"Project access must be granted explicitly to each user.":["L’accès au projet doit être explicitement accordé à chaque utilisateur."],"Project export could not be deleted.":["L'export du projet n'a pas pu être supprimé."],"Project export has been deleted.":["L'export du projet a été supprimé."],"#~ \"Project export link has expired. Please generate a new export from your projec\"#~ \"t settings.\"":["#~ \"Le lien de l’export du projet a expiré. Merci de générer un nouvel export depu\"#~ \"is les paramètres du projet.\""],"Project export started. A download link will be sent by email.":["#~ \"L'export du projet a débuté. Un lien de téléchargement sera envoyé par courrie\"#~ \"l.\""],"Project home":["Accueil du projet"],"ProjectFeature|Disabled":["Désactivé"],"ProjectFeature|Everyone with access":["Toute personne ayant accès"],"ProjectFeature|Only team members":["Seulement les membres de l'équipe"],"ProjectFileTree|Name":["Nom"],"ProjectLastActivity|Never":["Jamais"],"ProjectNetworkGraph|Graph":["Graphique "],"Readme":["LisezMoi"],"RefSwitcher|Branches":["Branches"],"RefSwitcher|Tags":["Étiquettes"],"Remind later":["Me le rappeler ultérieurement"],"Remove project":["Supprimer le projet"],"Request Access":["Demander l'accès"],"Revert this commit":["Annuler cette validation"],"Revert this merge-request":["Annuler cette demande de fusion"],"Save pipeline schedule":["Sauvegarder le pipeline programmé"],"Schedule a new pipeline":["Programmer un nouveau pipeline"],"Scheduling Pipelines":["Programmer des pipelines"],"Search branches and tags":["Rechercher dans les branches et les étiquettes"],"Select Archive Format":["Sélectionnez le format de l'archive"],"Select a timezone":["Sélectionnez un fuseau horaire"],"Select target branch":["Sélectionnez une branche cible"],"Set a password on your account to pull or push via %{protocol}":["#~ \"Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser par\"#~ \" %{protocol}\""],"Set up CI":["Mettre en place le CI"],"Set up Koding":["Mettre en place Koding"],"Set up auto deploy":["Mettre en place l’auto-déploiement "],"SetPasswordToCloneLink|set a password":["définir un mot de passe"],"Source code":["Code source"],"StarProject|Star":["S'abonner"],"Start a <strong>new merge request</strong> with these changes":["Créer une <strong>nouvelle demande de fusion</strong> avec ces changements"],"Switch branch/tag":["Changer de branche / d'étiquette"],"Tag":["Étiquette","Étiquettes"],"Tags":["Étiquettes"],"Target Branch":["Branche cible"],"The fork relationship has been removed.":["La relation de fork a été supprimée."],"#~ \"The pipelines schedule runs pipelines in the future, repeatedly, for specific \"#~ \"branches or tags. Those scheduled pipelines will inherit limited project acces\"#~ \"s based on their associated user.\"":["#~ \"Les pipelines programmés exécutent des pipelines dans le futur, de façon répét\"#~ \"ée, pour les branches et étiquettes spécifiées. Ces pipelines programmés hérit\"#~ \"ent d’un accès partiel au projet basé sur l’utilisateur que leurs est associé.\""],"The project can be accessed by any logged in user.":["Votre projet peut être accédé par n’importe quel utilisateur authentifié"],"The project can be accessed without any authentication.":["Votre projet peut être accédé sans aucune authentification."],"The repository for this project does not exist.":["Le dépôt pour ce projet n'existe pas."],"#~ \"This means you can not push code until you create an empty repository or impor\"#~ \"t existing one.\"":["#~ \"Cela signifie que vous ne pouvez pas pousser du code tant que vous ne créez pa\"#~ \"s un dépôt vide, ou importez une dépôt existant.\""],"Timeago|%s days ago":["Il y a %s jours"],"Timeago|%s days remaining":["Il reste %s jours"],"Timeago|%s hours remaining":["Il reste %s heures"],"Timeago|%s minutes ago":["Il y a %s minutes"],"Timeago|%s minutes remaining":["Il reste %s minutes"],"Timeago|%s months ago":["Il y a %s mois"],"Timeago|%s months remaining":["Il reste %s mois"],"Timeago|%s seconds remaining":["Il reste %s secondes"],"Timeago|%s weeks ago":["Il y a %s semaines"],"Timeago|%s weeks remaining":["Il reste %s semaines"],"Timeago|%s years ago":["Il y a %s ans"],"Timeago|%s years remaining":["Il reste %s ans"],"Timeago|1 day remaining":["Il reste un jour"],"Timeago|1 hour remaining":["Il reste une heure"],"Timeago|1 minute remaining":["Il reste une minute"],"Timeago|1 month remaining":["Il reste un mois"],"Timeago|1 week remaining":["Il reste une semaine"],"Timeago|1 year remaining":["Il reste un an"],"Timeago|Past due":["En retard"],"Timeago|a day ago":["Il y a un jour"],"Timeago|a month ago":["Il y a un mois"],"Timeago|a week ago":["Il y a une semaine"],"Timeago|a while":["Il y a un moment"],"Timeago|a year ago":["Il y a un an"],"Timeago|about %s hours ago":["Il y a environ %s heures"],"Timeago|about a minute ago":["Il y a environ une minute"],"Timeago|about an hour ago":["Il y a environ une heure"],"Timeago|in %s days":["Dans %s jours"],"Timeago|in %s hours":["Dans %s heures"],"Timeago|in %s minutes":["Dans %s minutes"],"Timeago|in %s months":["Dans %s mois"],"Timeago|in %s seconds":["Dans %s secondes"],"Timeago|in %s weeks":["Dans %s semaines"],"Timeago|in %s years":["Dans %s années"],"Timeago|in 1 day":["Dans 1 jour"],"Timeago|in 1 hour":["Dans 1 heure"],"Timeago|in 1 minute":["Dans 1 minute"],"Timeago|in 1 month":["Dans 1 mois"],"Timeago|in 1 week":["Dans 1 semaine"],"Timeago|in 1 year":["Dans 1 an"],"Timeago|less than a minute ago":["il y a moins d'une minute"],"Unstar":["Se désabonner"],"Upload New File":["Téléverser un nouveau fichier"],"Upload file":["Téléverser un fichier"],"Use your global notification setting":["Utiliser vos paramètres de notification globaux"],"VisibilityLevel|Internal":["Interne"],"VisibilityLevel|Private":["Privé"],"VisibilityLevel|Public":["Publique"],"Withdraw Access Request":["Retirer la demande d'accès"],"#~ \"You are going to remove %{project_name_with_namespace}.\\n\"#~ \"Removed project CANNOT be restored!\\n\"#~ \"Are you ABSOLUTELY sure?\"":["#~ \"Vous êtes sur le point de supprimer %{project_name_with_namespace}.\\n\"#~ \"Les projets supprimés NE PEUVENT PAS être restaurés !\\n\"#~ \"Êtes vous ABSOLUMENT sûr ? \""],"#~ \"You are going to remove the fork relationship to source project %{forked_from_\"#~ \"project}. Are you ABSOLUTELY sure?\"":["#~ \"Vous allez supprimer la relation de fork avec le projet source %{forked_from_p\"#~ \"roject}. Êtes-vous VRAIMENT sûr.\""],"#~ \"You are going to transfer %{project_name_with_namespace} to another owner. Are\"#~ \" you ABSOLUTELY sure?\"":["#~ \"Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire\"#~ \". Êtes vous VRAIMENT sûr ?\""],"You can only add files when you are on a branch":["Vous ne pouvez ajouter de fichier que dans une branche"],"You must sign in to star a project":["Vous devez vous identifier pour vous abonner à un projet"],"You will not get any notifications via email":["Vous ne recevrez aucune notification par courriel"],"You will only receive notifications for the events you choose":["#~ \"Vous ne recevrez de notification que pour les évènements que vous aurez choisi\"#~ \"s\""],"You will only receive notifications for threads you have participated in":["#~ \"Vous ne recevrez de notification que pour les sujets auxquels vous avez partic\"#~ \"ipé\""],"You will receive notifications for any activity":["Vous recevrez des notifications pour n’importe quelles activités"],"You will receive notifications only for comments in which you were @mentioned":["#~ \"Vous ne recevrez de notifications que pour les commentaires où vous êtes @ment\"#~ \"ionné\""],"#~ \"You won't be able to pull or push project code via %{protocol} until you %{set\"#~ \"_password_link} on your account\"":["#~ \"Vous ne pourrez pas récupérer ou pousser de code par %{protocol} tant que vo\"#~ \"us n'aurez pas %{set_password_link} pour votre compte\""],"#~ \"You won't be able to pull or push project code via SSH until you %{add_ssh_key\"#~ \"_link} to your profile\"":["#~ \"Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous n’aur\"#~ \"ez pas %{add_ssh_key_link} dans votre profil\""],"Your name":["Votre nom"],"notification emails":["courriels de notification"],"parent":["parent","parents"],"pipeline schedules documentation":["documentation des pipeline programmés"],"with stage":["avec l'étape","avec les étapes"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/pt_BR/app.js b/app/assets/javascripts/locale/pt_BR/app.js deleted file mode 100644 index f2eed3da064..00000000000 --- a/app/assets/javascripts/locale/pt_BR/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['pt_BR'] = {"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 03:29-0400","Last-Translator":"Alexandre Alencar <alexandre.alencar@gmail.com>","Language-Team":"Portuguese (Brazil)","Language":"pt-BR","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"pt_BR","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["por"],"Commit":["Commit","Commits"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora para ir para produção em seu projeto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Tarefa"],"CycleAnalyticsStage|Plan":["Plano"],"CycleAnalyticsStage|Production":["Produção"],"CycleAnalyticsStage|Review":["Revisão"],"CycleAnalyticsStage|Staging":["Homologação"],"CycleAnalyticsStage|Test":["Teste"],"Deploy":["Implantação","Implantações"],"FirstPushedBy|First":["Primeiro"],"FirstPushedBy|pushed by":["publicado por"],"From issue creation until deploy to production":["Da criação de tarefas até a implantação para a produção"],"From merge request merge until deploy to production":["Da incorporação do merge request até a implantação em produção"],"Introducing Cycle Analytics":["Apresentando a Análise de Ciclo"],"Last %d day":["Último %d dia","Últimos %d dias"],"Limited to showing %d event at most":["Limitado a mostrar %d evento no máximo","Limitado a mostrar %d eventos no máximo"],"Median":["Mediana"],"New Issue":["Nova Tarefa","Novas Tarefas"],"Not available":["Não disponível"],"Not enough data":["Dados insuficientes"],"OpenedNDaysAgo|Opened":["Aberto"],"Pipeline Health":["Saúde da Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Ler mais"],"Related Commits":["Commits Relacionados"],"Related Deployed Jobs":["Jobs Relacionados Incorporados"],"Related Issues":["Tarefas Relacionadas"],"Related Jobs":["Jobs Relacionados"],"Related Merge Requests":["Merge Requests Relacionados"],"Related Merged Requests":["Merge Requests Relacionados"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"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.":["O estágio de codificação mostra o tempo desde o primeiro commit até a criação do merge request. \\nOs dados serão automaticamente adicionados aqui uma vez que você tenha criado seu primeiro merge request."],"The collection of events added to the data gathered for that stage.":["A coleção de eventos adicionados aos dados coletados para esse estágio."],"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.":["O estágio em questão mostra o tempo que leva desde a criação de uma tarefa até a sua assinatura para um milestone, ou a sua adição para a lista no seu Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa."],"The phase of the development lifecycle.":["A fase do ciclo de vida do desenvolvimento."],"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.":["A fase de planejamento mostra o tempo do passo anterior até empurrar o seu primeiro commit. Este tempo será adicionado automaticamente assim que você realizar seu primeiro 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.":["O estágio de produção mostra o tempo total que leva entre criar uma tarefa e implantar o código na produção. Os dados serão adicionados automaticamente até que você complete todo o ciclo de produção."],"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.":["A etapa de revisão mostra o tempo de criação de um merge request até que o merge seja feito. Os dados serão automaticamente adicionados depois que você fizer seu primeiro 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.":["O estágio de estágio mostra o tempo entre a fusão do MR e o código de implantação para o ambiente de produção. Os dados serão automaticamente adicionados depois de implantar na produção pela primeira 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.":["A fase de teste mostra o tempo que o GitLab CI leva para executar cada pipeline para o merge request relacionado. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."],"The time taken by each data entry gathered by that stage.":["O tempo necessário para cada entrada de dados reunida por essa 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.":["O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tempo até que uma tarefa seja planejada"],"Time before an issue starts implementation":["Tempo até que uma tarefa comece a ser implementada"],"Time between merge request creation and merge/close":["Tempo entre a criação do merge request e o merge/fechamento"],"Time until first merge request":["Tempo até o primeiro merge request"],"Time|hr":["h","hs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tempo Total"],"Total test time for all commits/merges":["Tempo de teste total para todos os commits/merges"],"Want to see the data? Please ask an administrator for access.":["Precisa visualizar os dados? Solicite acesso ao administrador."],"We don't have enough data to show this stage.":["Não temos dados suficientes para mostrar esta fase."],"You need permission.":["Você precisa de permissão."],"day":["dia","dias"]}}};
\ 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 9c28e4e4627..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":"","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-19 09:57-0400","Last-Translator":"Huang Tao <htve@outlook.com>","Language-Team":"Chinese (China) (https://translate.zanata.org/project/view/GitLab)","Language":"zh-CN","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=1; plural=0","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0"},"%{commit_author_link} committed %{commit_timeago}":["由 %{commit_author_link} 提交于 %{commit_timeago}"],"About auto deploy":["关于自动部署"],"Active":["启用"],"Activity":["活动"],"Add Changelog":["添加更新日志"],"Add Contribution guide":["添加贡献指南"],"Add License":["添加许可证"],"Add an SSH key to your profile to pull or push via SSH.":["新建一个用于推送或拉取的 SSH 秘钥到账号中。"],"Add new directory":["添加目录"],"Archived project! Repository is read-only":["项目已归档!存储库为只读状态"],"Are you sure you want to delete this pipeline schedule?":["确定要删除此流水线计划吗?"],"Attach a file by drag & drop or %{upload_link}":["拖放文件到此处或者 %{upload_link}"],"Branch":["分支"],"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}":["已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择合适的 GitLab CI Yaml 模板并提交更改。%{link_to_autodeploy_doc}"],"Branches":["分支"],"Browse files":["浏览文件"],"ByAuthor|by":["作者:"],"CI configuration":["CI 配置"],"Cancel":["取消"],"ChangeTypeActionLabel|Pick into branch":["选择分支"],"ChangeTypeActionLabel|Revert in branch":["还原分支"],"ChangeTypeAction|Cherry-pick":["优选"],"ChangeTypeAction|Revert":["还原"],"Changelog":["更新日志"],"Charts":["统计图"],"Cherry-pick this commit":["优选此提交"],"Cherry-pick this merge request":["优选此合并请求"],"CiStatusLabel|canceled":["已取消"],"CiStatusLabel|created":["已创建"],"CiStatusLabel|failed":["已失败"],"CiStatusLabel|manual action":["手动操作"],"CiStatusLabel|passed":["已通过"],"CiStatusLabel|passed with warnings":["已通过但有警告"],"CiStatusLabel|pending":["等待中"],"CiStatusLabel|skipped":["已跳过"],"CiStatusLabel|waiting for manual action":["等待手动操作"],"CiStatusText|blocked":["已阻塞"],"CiStatusText|canceled":["已取消"],"CiStatusText|created":["已创建"],"CiStatusText|failed":["已失败"],"CiStatusText|manual":["手动操作"],"CiStatusText|passed":["已通过"],"CiStatusText|pending":["等待中"],"CiStatusText|skipped":["已跳过"],"CiStatus|running":["运行中"],"Commit":["提交"],"Commit message":["提交信息"],"CommitBoxTitle|Commit":["提交"],"CommitMessage|Add %{file_name}":["添加 %{file_name}"],"Commits":["提交"],"Commits|History":["历史"],"Committed by":["提交者:"],"Compare":["比较"],"Contribution guide":["贡献指南"],"Contributors":["贡献者"],"Copy URL to clipboard":["复制 URL 到剪贴板"],"Copy commit SHA to clipboard":["复制提交 SHA 的值到剪贴板"],"Create New Directory":["创建新目录"],"Create directory":["创建目录"],"Create empty bare repository":["创建空的存储库"],"Create merge request":["创建合并请求"],"Create new...":["创建..."],"CreateNewFork|Fork":["派生"],"CreateTag|Tag":["标签"],"Cron Timezone":["Cron 时区"],"Cron syntax":["Cron 语法"],"Custom notification events":["自定义通知事件"],"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}.":["自定义通知级别继承自参与级别。使用自定义通知级别,您会收到参与级别及选定事件的通知。想了解更多信息,请查看 %{notification_link}."],"Cycle Analytics":["周期分析"],"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":["测试"],"Define a custom pattern with cron syntax":["使用 Cron 语法定义自定义模式"],"Delete":["删除"],"Deploy":["部署"],"Description":["描述"],"Directory name":["目录名称"],"Don't show again":["不再显示"],"Download":["下载"],"Download tar":["下载 tar"],"Download tar.bz2":["下载 tar.bz2"],"Download tar.gz":["下载 tar.gz"],"Download zip":["下载 zip"],"DownloadArtifacts|Download":["下载"],"DownloadCommit|Email Patches":["电子邮件补丁"],"DownloadCommit|Plain Diff":["差异文件"],"DownloadSource|Download":["下载"],"Edit":["编辑"],"Edit Pipeline Schedule %{id}":["编辑 %{id} 流水线计划"],"Every day (at 4:00am)":["每日执行(凌晨 4 点)"],"Every month (on the 1st at 4:00am)":["每月执行(每月 1 日凌晨 4 点)"],"Every week (Sundays at 4:00am)":["每周执行(周日凌晨 4 点)"],"Failed to change the owner":["无法变更所有者"],"Failed to remove the pipeline schedule":["无法删除流水线计划"],"Files":["文件"],"Find by path":["按路径查找"],"Find file":["查找文件"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"Fork":["派生"],"ForkedFromProjectPath|Forked from":["派生自"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Go to your fork":["跳转到派生项目"],"GoToYourFork|Fork":["跳转到派生项目"],"Home":["首页"],"Housekeeping successfully started":["已开始维护"],"Import repository":["导入存储库"],"Interval Pattern":["循环周期"],"Introducing Cycle Analytics":["周期分析简介"],"LFSStatus|Disabled":["停用"],"LFSStatus|Enabled":["启用"],"Last %d day":["最近 %d 天"],"Last Pipeline":["最新流水线"],"Last Update":["最后更新"],"Last commit":["最后提交"],"Learn more in the":["了解更多"],"Learn more in the|pipeline schedules documentation":["流水线计划文档"],"Leave group":["退出群组"],"Leave project":["退出项目"],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"MissingSSHKeyWarningLink|add an SSH key":["新建 SSH 公钥"],"New Issue":["新建议题"],"New Pipeline Schedule":["创建流水线计划"],"New branch":["新建分支"],"New directory":["新建目录"],"New file":["新建文件"],"New issue":["新建议题"],"New merge request":["新建合并请求"],"New schedule":["新建计划"],"New snippet":["新建代码片段"],"New tag":["新建标签"],"No repository":["没有存储库"],"No schedules":["没有计划"],"Not available":["数据不足"],"Not enough data":["数据不足"],"Notification events":["通知事件"],"NotificationEvent|Close issue":["关闭议题"],"NotificationEvent|Close merge request":["关闭合并请求"],"NotificationEvent|Failed pipeline":["流水线失败"],"NotificationEvent|Merge merge request":["合并请求被合并"],"NotificationEvent|New issue":["新建议题"],"NotificationEvent|New merge request":["新建合并请求"],"NotificationEvent|New note":["新建评论"],"NotificationEvent|Reassign issue":["重新指派议题"],"NotificationEvent|Reassign merge request":["重新指派合并请求"],"NotificationEvent|Reopen issue":["重启议题"],"NotificationEvent|Successful pipeline":["流水线成功完成"],"NotificationLevel|Custom":["自定义"],"NotificationLevel|Disabled":["停用"],"NotificationLevel|Global":["全局"],"NotificationLevel|On mention":["提及"],"NotificationLevel|Participate":["参与"],"NotificationLevel|Watch":["关注"],"OfSearchInADropdown|Filter":["筛选"],"OpenedNDaysAgo|Opened":["开始于"],"Options":["操作"],"Owner":["所有者"],"Pipeline":["流水线"],"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":["目标"],"PipelineSheduleIntervalPattern|Custom":["自定义"],"Pipeline|with stage":["于阶段"],"Pipeline|with stages":["于阶段"],"Project '%{project_name}' queued for deletion.":["项目 '%{project_name}' 已进入删除队列。"],"Project '%{project_name}' was successfully created.":["项目 '%{project_name}' 已创建成功。"],"Project '%{project_name}' was successfully updated.":["项目 '%{project_name}' 已更新完成。"],"Project '%{project_name}' will be deleted.":["项目 '%{project_name}' 将被删除。"],"Project access must be granted explicitly to each user.":["项目访问权限必须明确授权给每个用户。"],"Project export could not be deleted.":["无法删除项目导出。"],"Project export has been deleted.":["项目导出已被删除。"],"Project export link has expired. Please generate a new export from your project settings.":["项目导出链接已过期。请从项目设置中重新生成项目导出。"],"Project export started. A download link will be sent by email.":["项目导出已开始。下载链接将通过电子邮件发送。"],"Project home":["项目首页"],"ProjectFeature|Disabled":["停用"],"ProjectFeature|Everyone with access":["任何对项目有访问权的人"],"ProjectFeature|Only team members":["只限团队成员"],"ProjectFileTree|Name":["名称"],"ProjectLastActivity|Never":["从未"],"ProjectLifecycle|Stage":["阶段"],"ProjectNetworkGraph|Graph":["分支图"],"Read more":["了解更多"],"Readme":["自述文件"],"RefSwitcher|Branches":["分支"],"RefSwitcher|Tags":["标签"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Remind later":["稍后提醒"],"Remove project":["删除项目"],"Request Access":["申请权限"],"Revert this commit":["还原此提交"],"Revert this merge request":["还原此合并请求"],"Save pipeline schedule":["保存流水线计划"],"Schedule a new pipeline":["新建流水线计划"],"Scheduling Pipelines":["流水线计划"],"Search branches and tags":["搜索分支和标签"],"Select Archive Format":["选择下载格式"],"Select a timezone":["选择时区"],"Select target branch":["选择目标分支"],"Set a password on your account to pull or push via %{protocol}":["为账号创建一个用于推送或拉取的 %{protocol} 密码。"],"Set up CI":["设置 CI"],"Set up Koding":["设置 Koding"],"Set up auto deploy":["设置自动部署"],"SetPasswordToCloneLink|set a password":["设置密码"],"Showing %d event":["显示 %d 个事件"],"Source code":["源代码"],"StarProject|Star":["星标"],"Start a %{new_merge_request} with these changes":["由此更改 %{new_merge_request}"],"Switch branch/tag":["切换分支/标签"],"Tag":["标签"],"Tags":["标签"],"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 fork relationship has been removed.":["派生关系已被删除。"],"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 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 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 project can be accessed by any logged in user.":["该项目允许已登录的用户访问。"],"The project can be accessed without any authentication.":["该项目允许任何人访问。"],"The repository for this project does not exist.":["此项目的存储库不存在。"],"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。"],"This means you can not push code until you create an empty repository or import existing one.":["在创建一个空的存储库或导入现有存储库之前,将无法推送代码。"],"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":["创建第一个合并请求之前的时间"],"Timeago|%s days ago":[" %s 天前"],"Timeago|%s days remaining":["剩余 %s 天"],"Timeago|%s hours remaining":["剩余 %s 小时"],"Timeago|%s minutes ago":[" %s 分钟前"],"Timeago|%s minutes remaining":["剩余 %s 分钟"],"Timeago|%s months ago":[" %s 个月前"],"Timeago|%s months remaining":["剩余 %s 月"],"Timeago|%s seconds remaining":["剩余 %s 秒"],"Timeago|%s weeks ago":[" %s 星期前"],"Timeago|%s weeks remaining":["剩余 %s 星期"],"Timeago|%s years ago":[" %s 年前"],"Timeago|%s years remaining":["剩余 %s 年"],"Timeago|1 day remaining":["剩余 1 天"],"Timeago|1 hour remaining":["剩余 1 小时"],"Timeago|1 minute remaining":["剩余 1 分钟"],"Timeago|1 month remaining":["剩余 1 个月"],"Timeago|1 week remaining":["剩余 1 星期"],"Timeago|1 year remaining":["剩余 1 年"],"Timeago|Past due":["逾期"],"Timeago|a day ago":[" 1 天前"],"Timeago|a month ago":[" 1 个月前"],"Timeago|a week ago":[" 1 星期前"],"Timeago|a while":["刚刚"],"Timeago|a year ago":[" 1 年前"],"Timeago|about %s hours ago":["约 %s 小时前"],"Timeago|about a minute ago":["约 1 分钟前"],"Timeago|about an hour ago":["约 1 小时前"],"Timeago|in %s days":[" %s 天后"],"Timeago|in %s hours":[" %s 小时后"],"Timeago|in %s minutes":[" %s 分钟后"],"Timeago|in %s months":[" %s 个月后"],"Timeago|in %s seconds":[" %s 秒后"],"Timeago|in %s weeks":[" %s 星期后"],"Timeago|in %s years":[" %s 年后"],"Timeago|in 1 day":[" 1 天后"],"Timeago|in 1 hour":[" 1 小时后"],"Timeago|in 1 minute":[" 1 分钟后"],"Timeago|in 1 month":[" 1 月后"],"Timeago|in 1 week":[" 1 星期后"],"Timeago|in 1 year":[" 1 年后"],"Timeago|less than a minute ago":["不到 1 分钟前"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Unstar":["取消星标"],"Upload New File":["上传新文件"],"Upload file":["上传文件"],"Use your global notification setting":["使用全局通知设置"],"VisibilityLevel|Internal":["内部"],"VisibilityLevel|Private":["私有"],"VisibilityLevel|Public":["公开"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"Withdraw Access Request":["取消权限申请"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["即将要删除 %{project_name_with_namespace}。\\n已删除的项目无法恢复!\\n确定继续吗?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["即将删除与源项目 %{forked_from_project} 的派生关系。确定继续吗?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["即将 %{project_name_with_namespace} 转移给另一个所有者。确定继续吗?"],"You can only add files when you are on a branch":["只能在分支上添加文件"],"You have reached your project limit":["您已达到项目数量限制"],"You must sign in to star a project":["必须登录才能对项目加星标"],"You need permission.":["需要相关的权限。"],"You will not get any notifications via email":["不会收到任何通知邮件"],"You will only receive notifications for the events you choose":["只接收选择的事件通知"],"You will only receive notifications for threads you have participated in":["只接收参与的主题的通知"],"You will receive notifications for any activity":["接收所有活动的通知"],"You will receive notifications only for comments in which you were @mentioned":["只接收评论中提及(@)您的通知"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["在账号中 %{set_password_link} 之前将无法通过 %{protocol} 拉取或推送代码。"],"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} 之前将无法通过 SSH 拉取或推送代码。"],"Your name":["您的名字"],"day":["天"],"new merge request":["新建合并请求"],"notification emails":["通知邮件"],"parent":["父级"]}}};
\ 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 786b6014dc6..7840f05a8ae 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.resetViewContainer(); this.mountPipelinesView(); } else { - this.expandView(); + if (Breakpoints.get().getBreakpointSize() !== 'xs') { + this.expandView(); + } this.resetViewContainer(); this.destroyPipelinesView(); } @@ -168,9 +170,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Activate a tab based on the current action activateTab(action) { - const activate = action === 'show' ? 'notes' : action; // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(`.merge-request-tabs a[data-action='${activate}']`).tab('show'); + $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); } // Replaces the current Merge Request-specific action in the URL with a new one @@ -185,7 +186,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // location.pathname # => "/namespace/project/merge_requests/1/diffs" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('notes') + // setCurrentAction('show') // location.pathname # => "/namespace/project/merge_requests/1" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" @@ -194,13 +195,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // // Returns the new URL String setCurrentAction(action) { - this.currentAction = action === 'show' ? 'notes' : action; + this.currentAction = action; - // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' - let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + // Remove a trailing '/commits' '/diffs' '/pipelines' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' - if (this.currentAction !== 'notes') { + if (this.currentAction !== 'show' && this.currentAction !== 'new') { newState += `/${this.currentAction}`; } diff --git a/app/assets/javascripts/monitoring/components/monitoring.vue b/app/assets/javascripts/monitoring/components/monitoring.vue new file mode 100644 index 00000000000..a6a2d3119e3 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring.vue @@ -0,0 +1,157 @@ +<script> + /* global Flash */ + import _ from 'underscore'; + import statusCodes from '../../lib/utils/http_status'; + import MonitoringService from '../services/monitoring_service'; + import monitoringRow from './monitoring_row.vue'; + import monitoringState from './monitoring_state.vue'; + import MonitoringStore from '../stores/monitoring_store'; + import eventHub from '../event_hub'; + + export default { + + data() { + const metricsData = document.querySelector('#prometheus-graphs').dataset; + const store = new MonitoringStore(); + + return { + store, + state: 'gettingStarted', + hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), + documentationPath: metricsData.documentationPath, + settingsPath: metricsData.settingsPath, + endpoint: metricsData.additionalMetrics, + deploymentEndpoint: metricsData.deploymentEndpoint, + showEmptyState: true, + backOffRequestCounter: 0, + updateAspectRatio: false, + updatedAspectRatios: 0, + resizeThrottled: {}, + }; + }, + + components: { + monitoringRow, + monitoringState, + }, + + methods: { + getGraphsData() { + const maxNumberOfRequests = 3; + this.state = 'loading'; + gl.utils.backOff((next, stop) => { + this.service.get().then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + if (this.backOffRequestCounter < maxNumberOfRequests) { + next(); + } else { + stop(new Error('Failed to connect to the prometheus server')); + } + } else { + stop(resp); + } + }).catch(stop); + }) + .then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + this.state = 'unableToConnect'; + return false; + } + return resp.json(); + }) + .then((metricGroupsData) => { + if (!metricGroupsData) return false; + this.store.storeMetrics(metricGroupsData.data); + return this.getDeploymentData(); + }) + .then((deploymentData) => { + if (deploymentData !== false) { + this.store.storeDeploymentData(deploymentData.deployments); + this.showEmptyState = false; + } + return {}; + }) + .catch(() => { + this.state = 'unableToConnect'; + }); + }, + + getDeploymentData() { + return this.service.getDeploymentData(this.deploymentEndpoint) + .then(resp => resp.json()) + .catch(() => new Flash('Error getting deployment information.')); + }, + + resize() { + this.updateAspectRatio = true; + }, + + toggleAspectRatio() { + this.updatedAspectRatios = this.updatedAspectRatios += 1; + if (this.store.getMetricsCount() === this.updatedAspectRatios) { + this.updateAspectRatio = !this.updateAspectRatio; + this.updatedAspectRatios = 0; + } + }, + + }, + + created() { + this.service = new MonitoringService(this.endpoint); + eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + }, + + beforeDestroy() { + eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); + window.removeEventListener('resize', this.resizeThrottled, false); + }, + + mounted() { + this.resizeThrottled = _.throttle(this.resize, 600); + if (!this.hasMetrics) { + this.state = 'gettingStarted'; + } else { + this.getGraphsData(); + window.addEventListener('resize', this.resizeThrottled, false); + } + }, + }; +</script> +<template> + <div + class="prometheus-graphs" + v-if="!showEmptyState"> + <div + class="row" + v-for="(groupData, index) in store.groups" + :key="index"> + <div + class="col-md-12"> + <div + class="panel panel-default prometheus-panel"> + <div + class="panel-heading"> + <h4>{{groupData.group}}</h4> + </div> + <div + class="panel-body"> + <monitoring-row + v-for="(row, index) in groupData.metrics" + :key="index" + :row-data="row" + :update-aspect-ratio="updateAspectRatio" + :deployment-data="store.deploymentData" + /> + </div> + </div> + </div> + </div> + </div> + <monitoring-state + :selected-state="state" + :documentation-path="documentationPath" + :settings-path="settingsPath" + v-else + /> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue new file mode 100644 index 00000000000..4f4792877ee --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_column.vue @@ -0,0 +1,291 @@ +<script> + /* global Breakpoints */ + import d3 from 'd3'; + import monitoringLegends from './monitoring_legends.vue'; + import monitoringFlag from './monitoring_flag.vue'; + import monitoringDeployment from './monitoring_deployment.vue'; + import MonitoringMixin from '../mixins/monitoring_mixins'; + import eventHub from '../event_hub'; + import measurements from '../utils/measurements'; + import { formatRelevantDigits } from '../../lib/utils/number_utils'; + + const bisectDate = d3.bisector(d => d.time).left; + + export default { + props: { + columnData: { + type: Object, + required: true, + }, + classType: { + type: String, + required: true, + }, + updateAspectRatio: { + type: Boolean, + required: true, + }, + deploymentData: { + type: Array, + required: true, + }, + }, + + mixins: [MonitoringMixin], + + data() { + return { + graphHeight: 500, + graphWidth: 600, + graphHeightOffset: 120, + xScale: {}, + yScale: {}, + margin: {}, + data: [], + breakpointHandler: Breakpoints.get(), + unitOfDisplay: '', + areaColorRgb: '#8fbce8', + lineColorRgb: '#1f78d1', + yAxisLabel: '', + legendTitle: '', + reducedDeploymentData: [], + area: '', + line: '', + measurements: measurements.large, + currentData: { + time: new Date(), + value: 0, + }, + currentYCoordinate: 0, + currentXCoordinate: 0, + currentFlagPosition: 0, + metricUsage: '', + showFlag: false, + showDeployInfo: true, + }; + }, + + components: { + monitoringLegends, + monitoringFlag, + monitoringDeployment, + }, + + computed: { + outterViewBox() { + return `0 0 ${this.graphWidth} ${this.graphHeight}`; + }, + + innerViewBox() { + if ((this.graphWidth - 150) > 0) { + return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`; + } + return '0 0 0 0'; + }, + + axisTransform() { + return `translate(70, ${this.graphHeight - 100})`; + }, + + paddingBottomRootSvg() { + return (Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0; + }, + }, + + methods: { + draw() { + const breakpointSize = this.breakpointHandler.getBreakpointSize(); + const query = this.columnData.queries[0]; + this.margin = measurements.large.margin; + if (breakpointSize === 'xs' || breakpointSize === 'sm') { + this.graphHeight = 300; + this.margin = measurements.small.margin; + this.measurements = measurements.small; + } + this.data = query.result[0].values; + this.unitOfDisplay = query.unit || 'N/A'; + this.yAxisLabel = this.columnData.y_axis || 'Values'; + this.legendTitle = query.legend || 'Average'; + this.graphWidth = this.$refs.baseSvg.clientWidth - + this.margin.left - this.margin.right; + this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; + if (this.data !== undefined) { + this.renderAxesPaths(); + this.formatDeployments(); + } + }, + + handleMouseOverGraph(e) { + let point = this.$refs.graphData.createSVGPoint(); + point.x = e.clientX; + point.y = e.clientY; + point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); + point.x = point.x += 7; + const timeValueOverlay = this.xScale.invert(point.x); + const overlayIndex = bisectDate(this.data, timeValueOverlay, 1); + const d0 = this.data[overlayIndex - 1]; + const d1 = this.data[overlayIndex]; + if (d0 === undefined || d1 === undefined) return; + const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; + this.currentData = evalTime ? d1 : d0; + this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time)); + const currentDeployXPos = this.mouseOverDeployInfo(point.x); + this.currentYCoordinate = this.yScale(this.currentData.value); + + if (this.currentXCoordinate > (this.graphWidth - 200)) { + this.currentFlagPosition = this.currentXCoordinate - 103; + } else { + this.currentFlagPosition = this.currentXCoordinate; + } + + if (currentDeployXPos) { + this.showFlag = false; + } else { + this.showFlag = true; + } + + this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`; + }, + + renderAxesPaths() { + const axisXScale = d3.time.scale() + .range([0, this.graphWidth]); + this.yScale = d3.scale.linear() + .range([this.graphHeight - this.graphHeightOffset, 0]); + axisXScale.domain(d3.extent(this.data, d => d.time)); + this.yScale.domain([0, d3.max(this.data.map(d => d.value))]); + + const xAxis = d3.svg.axis() + .scale(axisXScale) + .ticks(measurements.ticks) + .orient('bottom'); + + const yAxis = d3.svg.axis() + .scale(this.yScale) + .ticks(measurements.ticks) + .orient('left'); + + d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis); + + const width = this.graphWidth; + d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis) + .selectAll('.tick') + .each(function createTickLines() { + d3.select(this).select('line').attr('x2', width); + }); // This will select all of the ticks once they're rendered + + this.xScale = d3.time.scale() + .range([0, this.graphWidth - 70]); + + this.xScale.domain(d3.extent(this.data, d => d.time)); + + const areaFunction = d3.svg.area() + .x(d => this.xScale(d.time)) + .y0(this.graphHeight - this.graphHeightOffset) + .y1(d => this.yScale(d.value)) + .interpolate('linear'); + + const lineFunction = d3.svg.line() + .x(d => this.xScale(d.time)) + .y(d => this.yScale(d.value)); + + this.line = lineFunction(this.data); + + this.area = areaFunction(this.data); + }, + }, + + watch: { + updateAspectRatio() { + if (this.updateAspectRatio) { + this.graphHeight = 500; + this.graphWidth = 600; + this.measurements = measurements.large; + this.draw(); + eventHub.$emit('toggleAspectRatio'); + } + }, + }, + + mounted() { + this.draw(); + }, + }; +</script> +<template> + <div + :class="classType"> + <h5 + class="text-center"> + {{columnData.title}} + </h5> + <div + class="prometheus-svg-container"> + <svg + :viewBox="outterViewBox" + :style="{ 'padding-bottom': paddingBottomRootSvg }" + ref="baseSvg"> + <g + class="x-axis" + :transform="axisTransform"> + </g> + <g + class="y-axis" + transform="translate(70, 20)"> + </g> + <monitoring-legends + :graph-width="graphWidth" + :graph-height="graphHeight" + :margin="margin" + :measurements="measurements" + :area-color-rgb="areaColorRgb" + :legend-title="legendTitle" + :y-axis-label="yAxisLabel" + :metric-usage="metricUsage" + /> + <svg + class="graph-data" + :viewBox="innerViewBox" + ref="graphData"> + <path + class="metric-area" + :d="area" + :fill="areaColorRgb" + transform="translate(-5, 20)"> + </path> + <path + class="metric-line" + :d="line" + :stroke="lineColorRgb" + fill="none" + stroke-width="2" + transform="translate(-5, 20)"> + </path> + <rect + class="prometheus-graph-overlay" + :width="(graphWidth - 70)" + :height="(graphHeight - 100)" + transform="translate(-5, 20)" + ref="graphOverlay" + @mousemove="handleMouseOverGraph($event)"> + </rect> + <monitoring-deployment + :show-deploy-info="showDeployInfo" + :deployment-data="reducedDeploymentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> + <monitoring-flag + v-if="showFlag" + :current-x-coordinate="currentXCoordinate" + :current-y-coordinate="currentYCoordinate" + :current-data="currentData" + :current-flag-position="currentFlagPosition" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> + </svg> + </svg> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_deployment.vue b/app/assets/javascripts/monitoring/components/monitoring_deployment.vue new file mode 100644 index 00000000000..e6432ba3191 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_deployment.vue @@ -0,0 +1,136 @@ +<script> + import { + dateFormat, + timeFormat, + } from '../constants'; + + export default { + props: { + showDeployInfo: { + type: Boolean, + required: true, + }, + deploymentData: { + type: Array, + required: true, + }, + graphHeight: { + type: Number, + required: true, + }, + graphHeightOffset: { + type: Number, + required: true, + }, + }, + + computed: { + calculatedHeight() { + return this.graphHeight - this.graphHeightOffset; + }, + }, + + methods: { + refText(d) { + return d.tag ? d.ref : d.sha.slice(0, 6); + }, + + formatTime(deploymentTime) { + return timeFormat(deploymentTime); + }, + + formatDate(deploymentTime) { + return dateFormat(deploymentTime); + }, + + nameDeploymentClass(deployment) { + return `deploy-info-${deployment.id}`; + }, + + transformDeploymentGroup(deployment) { + return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; + }, + }, + }; +</script> +<template> + <g + class="deploy-info" + v-if="showDeployInfo"> + <g + v-for="(deployment, index) in deploymentData" + :key="index" + :class="nameDeploymentClass(deployment)" + :transform="transformDeploymentGroup(deployment)"> + <rect + x="0" + y="0" + :height="calculatedHeight" + width="3" + fill="url(#shadow-gradient)"> + </rect> + <line + class="deployment-line" + x1="0" + y1="0" + x2="0" + :y2="calculatedHeight" + stroke="#000"> + </line> + <svg + v-if="deployment.showDeploymentFlag" + class="js-deploy-info-box" + x="3" + y="0" + width="92" + height="60"> + <rect + class="rect-text-metric deploy-info-rect rect-metric" + x="1" + y="1" + rx="2" + width="90" + height="58"> + </rect> + <g + transform="translate(5, 2)"> + <text + class="deploy-info-text text-metric-bold"> + {{refText(deployment)}} + </text> + </g> + <text + class="deploy-info-text" + y="18" + transform="translate(5, 2)"> + {{formatDate(deployment.time)}} + </text> + <text + class="deploy-info-text text-metric-bold" + y="38" + transform="translate(5, 2)"> + {{formatTime(deployment.time)}} + </text> + </svg> + </g> + <svg + height="0" + width="0"> + <defs> + <linearGradient + id="shadow-gradient"> + <stop + offset="0%" + stop-color="#000" + stop-opacity="0.4"> + </stop> + <stop + offset="100%" + stop-color="#000" + stop-opacity="0"> + </stop> + </linearGradient> + </defs> + </svg> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_flag.vue b/app/assets/javascripts/monitoring/components/monitoring_flag.vue new file mode 100644 index 00000000000..180a771415b --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_flag.vue @@ -0,0 +1,104 @@ +<script> + import { + dateFormat, + timeFormat, + } from '../constants'; + + export default { + props: { + currentXCoordinate: { + type: Number, + required: true, + }, + currentYCoordinate: { + type: Number, + required: true, + }, + currentFlagPosition: { + type: Number, + required: true, + }, + currentData: { + type: Object, + required: true, + }, + graphHeight: { + type: Number, + required: true, + }, + graphHeightOffset: { + type: Number, + required: true, + }, + }, + + data() { + return { + circleColorRgb: '#8fbce8', + }; + }, + + computed: { + formatTime() { + return timeFormat(this.currentData.time); + }, + + formatDate() { + return dateFormat(this.currentData.time); + }, + + calculatedHeight() { + return this.graphHeight - this.graphHeightOffset; + }, + }, + }; +</script> +<template> + <g class="mouse-over-flag"> + <line + class="selected-metric-line" + :x1="currentXCoordinate" + :y1="0" + :x2="currentXCoordinate" + :y2="calculatedHeight" + transform="translate(-5, 20)"> + </line> + <circle + class="circle-metric" + :fill="circleColorRgb" + stroke="#000" + :cx="currentXCoordinate" + :cy="currentYCoordinate" + r="5" + transform="translate(-5, 20)"> + </circle> + <svg + class="rect-text-metric" + :x="currentFlagPosition" + y="0"> + <rect + class="rect-metric" + x="4" + y="1" + rx="2" + width="90" + height="40" + transform="translate(-3, 20)"> + </rect> + <text + class="text-metric text-metric-bold" + x="8" + y="35" + transform="translate(-5, 20)"> + {{formatTime}} + </text> + <text + class="text-metric-date" + x="8" + y="15" + transform="translate(-5, 20)"> + {{formatDate}} + </text> + </svg> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_legends.vue b/app/assets/javascripts/monitoring/components/monitoring_legends.vue new file mode 100644 index 00000000000..b30ed3cc889 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_legends.vue @@ -0,0 +1,144 @@ +<script> + export default { + props: { + graphWidth: { + type: Number, + required: true, + }, + graphHeight: { + type: Number, + required: true, + }, + margin: { + type: Object, + required: true, + }, + measurements: { + type: Object, + required: true, + }, + areaColorRgb: { + type: String, + required: true, + }, + legendTitle: { + type: String, + required: true, + }, + yAxisLabel: { + type: String, + required: true, + }, + metricUsage: { + type: String, + required: true, + }, + }, + data() { + return { + yLabelWidth: 0, + yLabelHeight: 0, + }; + }, + computed: { + textTransform() { + const yCoordinate = (((this.graphHeight - this.margin.top) + + this.measurements.axisLabelLineOffset) / 2) || 0; + + return `translate(15, ${yCoordinate}) rotate(-90)`; + }, + + rectTransform() { + const yCoordinate = ((this.graphHeight - this.margin.top) / 2) + + (this.yLabelWidth / 2) + 10 || 0; + + return `translate(0, ${yCoordinate}) rotate(-90)`; + }, + + xPosition() { + return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2) + - this.margin.right) || 0; + }, + + yPosition() { + return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; + }, + }, + mounted() { + this.$nextTick(() => { + const bbox = this.$refs.ylabel.getBBox(); + this.yLabelWidth = bbox.width + 10; // Added some padding + this.yLabelHeight = bbox.height + 5; + }); + }, + }; +</script> +<template> + <g + class="axis-label-container"> + <line + class="label-x-axis-line" + stroke="#000000" + stroke-width="1" + x1="10" + :y1="yPosition" + :x2="graphWidth + 20" + :y2="yPosition"> + </line> + <line + class="label-y-axis-line" + stroke="#000000" + stroke-width="1" + x1="10" + y1="0" + :x2="10" + :y2="yPosition"> + </line> + <rect + class="rect-axis-text" + :transform="rectTransform" + :width="yLabelWidth" + :height="yLabelHeight"> + </rect> + <text + class="label-axis-text y-label-text" + text-anchor="middle" + :transform="textTransform" + ref="ylabel"> + {{yAxisLabel}} + </text> + <rect + class="rect-axis-text" + :x="xPosition + 50" + :y="graphHeight - 80" + width="50" + height="50"> + </rect> + <text + class="label-axis-text" + :x="xPosition + 60" + :y="yPosition" + dy=".35em"> + Time + </text> + <rect + :fill="areaColorRgb" + :width="measurements.legends.width" + :height="measurements.legends.height" + x="20" + :y="graphHeight - measurements.legendOffset"> + </rect> + <text + class="text-metric-title" + x="50" + :y="graphHeight - 40"> + {{legendTitle}} + </text> + <text + class="text-metric-usage" + x="50" + :y="graphHeight - 25"> + {{metricUsage}} + </text> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_row.vue b/app/assets/javascripts/monitoring/components/monitoring_row.vue new file mode 100644 index 00000000000..e5528f17880 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_row.vue @@ -0,0 +1,41 @@ +<script> + import monitoringColumn from './monitoring_column.vue'; + + export default { + props: { + rowData: { + type: Array, + required: true, + }, + updateAspectRatio: { + type: Boolean, + required: true, + }, + deploymentData: { + type: Array, + required: true, + }, + }, + components: { + monitoringColumn, + }, + computed: { + bootstrapClass() { + return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12'; + }, + }, + }; +</script> +<template> + <div + class="prometheus-row row"> + <monitoring-column + v-for="(column, index) in rowData" + :column-data="column" + :class-type="bootstrapClass" + :key="index" + :update-aspect-ratio="updateAspectRatio" + :deployment-data="deploymentData" + /> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_state.vue b/app/assets/javascripts/monitoring/components/monitoring_state.vue new file mode 100644 index 00000000000..598021aa4df --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_state.vue @@ -0,0 +1,112 @@ +<script> + import gettingStartedSvg from 'empty_states/monitoring/_getting_started.svg'; + import loadingSvg from 'empty_states/monitoring/_loading.svg'; + import unableToConnectSvg from 'empty_states/monitoring/_unable_to_connect.svg'; + + export default { + props: { + documentationPath: { + type: String, + required: true, + }, + settingsPath: { + type: String, + required: false, + default: '', + }, + selectedState: { + type: String, + required: true, + }, + }, + data() { + return { + states: { + gettingStarted: { + svg: gettingStartedSvg, + title: 'Get started with performance monitoring', + description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.', + buttonText: 'Configure Prometheus', + }, + loading: { + svg: loadingSvg, + title: 'Waiting for performance data', + description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.', + buttonText: 'View documentation', + }, + unableToConnect: { + svg: unableToConnectSvg, + title: 'Unable to connect to Prometheus server', + description: 'Ensure connectivity is available from the GitLab server to the ', + buttonText: 'View documentation', + }, + }, + }; + }, + computed: { + currentState() { + return this.states[this.selectedState]; + }, + + buttonPath() { + if (this.selectedState === 'gettingStarted') { + return this.settingsPath; + } + return this.documentationPath; + }, + + showButtonDescription() { + if (this.selectedState === 'unableToConnect') return true; + return false; + }, + }, + }; +</script> +<template> + <div + class="prometheus-state"> + <div + class="row"> + <div + class="col-md-4 col-md-offset-4 state-svg" + v-html="currentState.svg"> + </div> + </div> + <div + class="row"> + <div + class="col-md-6 col-md-offset-3"> + <h4 + class="text-center state-title"> + {{currentState.title}} + </h4> + </div> + </div> + <div + class="row"> + <div + class="col-md-6 col-md-offset-3"> + <div + class="description-text text-center state-description"> + {{currentState.description}} + <a + :href="settingsPath" + v-if="showButtonDescription"> + Prometheus server + </a> + </div> + </div> + </div> + <div + class="row state-button-section"> + <div + class="col-md-4 col-md-offset-4 text-center state-button"> + <a + class="btn btn-success" + :href="buttonPath"> + {{currentState.buttonText}} + </a> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/deployments.js b/app/assets/javascripts/monitoring/deployments.js deleted file mode 100644 index fc92ab61b31..00000000000 --- a/app/assets/javascripts/monitoring/deployments.js +++ /dev/null @@ -1,211 +0,0 @@ -/* global Flash */ -import d3 from 'd3'; -import { - dateFormat, - timeFormat, -} from './constants'; - -export default class Deployments { - constructor(width, height) { - this.width = width; - this.height = height; - - this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint; - - this.createGradientDef(); - } - - init(chartData) { - this.chartData = chartData; - - this.x = d3.time.scale().range([0, this.width]); - this.x.domain(d3.extent(this.chartData, d => d.time)); - - this.charts = d3.selectAll('.prometheus-graph'); - - this.getData(); - } - - getData() { - $.ajax({ - url: this.endpoint, - dataType: 'JSON', - }) - .fail(() => new Flash('Error getting deployment information.')) - .done((data) => { - this.data = data.deployments.reduce((deploymentDataArray, deployment) => { - const time = new Date(deployment.created_at); - const xPos = Math.floor(this.x(time)); - - time.setSeconds(this.chartData[0].time.getSeconds()); - - if (xPos >= 0) { - deploymentDataArray.push({ - id: deployment.id, - time, - sha: deployment.sha, - tag: deployment.tag, - ref: deployment.ref.name, - xPos, - }); - } - - return deploymentDataArray; - }, []); - - this.plotData(); - }); - } - - plotData() { - this.charts.each((d, i) => { - const svg = d3.select(this.charts[0][i]); - const chart = svg.select('.graph-container'); - const key = svg.node().getAttribute('graph-type'); - - this.createLine(chart, key); - this.createDeployInfoBox(chart, key); - }); - } - - createGradientDef() { - const defs = d3.select('body') - .append('svg') - .attr({ - height: 0, - width: 0, - }) - .append('defs'); - - defs.append('linearGradient') - .attr({ - id: 'shadow-gradient', - }) - .append('stop') - .attr({ - offset: '0%', - 'stop-color': '#000', - 'stop-opacity': 0.4, - }) - .select(this.selectParentNode) - .append('stop') - .attr({ - offset: '100%', - 'stop-color': '#000', - 'stop-opacity': 0, - }); - } - - createLine(chart, key) { - chart.append('g') - .attr({ - class: 'deploy-info', - }) - .selectAll('.deploy-info') - .data(this.data) - .enter() - .append('g') - .attr({ - class: d => `deploy-info-${d.id}-${key}`, - transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`, - }) - .append('rect') - .attr({ - x: 1, - y: 0, - height: this.height + 1, - width: 3, - fill: 'url(#shadow-gradient)', - }) - .select(this.selectParentNode) - .append('line') - .attr({ - class: 'deployment-line', - x1: 0, - x2: 0, - y1: 0, - y2: this.height + 1, - }); - } - - createDeployInfoBox(chart, key) { - chart.selectAll('.deploy-info') - .selectAll('.js-deploy-info-box') - .data(this.data) - .enter() - .select(d => document.querySelector(`.deploy-info-${d.id}-${key}`)) - .append('svg') - .attr({ - class: 'js-deploy-info-box hidden', - x: 3, - y: 0, - width: 92, - height: 60, - }) - .append('rect') - .attr({ - class: 'rect-text-metric deploy-info-rect rect-metric', - x: 1, - y: 1, - rx: 2, - width: 90, - height: 58, - }) - .select(this.selectParentNode) - .append('g') - .attr({ - transform: 'translate(5, 2)', - }) - .append('text') - .attr({ - class: 'deploy-info-text text-metric-bold', - }) - .text(Deployments.refText) - .select(this.selectParentNode) - .append('text') - .attr({ - class: 'deploy-info-text', - y: 18, - }) - .text(d => dateFormat(d.time)) - .select(this.selectParentNode) - .append('text') - .attr({ - class: 'deploy-info-text text-metric-bold', - y: 38, - }) - .text(d => timeFormat(d.time)); - } - - static toggleDeployTextbox(deploy, key, showInfoBox) { - d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`) - .classed('hidden', !showInfoBox); - } - - mouseOverDeployInfo(mouseXPos, key) { - if (!this.data) return false; - - let dataFound = false; - - this.data.forEach((d) => { - if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { - dataFound = d.xPos + 1; - - Deployments.toggleDeployTextbox(d, key, true); - } else { - Deployments.toggleDeployTextbox(d, key, false); - } - }); - - return dataFound; - } - - /* `this` is bound to the D3 node */ - selectParentNode() { - return this.parentNode; - } - - static refText(d) { - return d.tag ? d.ref : d.sha.slice(0, 6); - } -} diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/monitoring/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/monitoring/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js new file mode 100644 index 00000000000..8e62fa63f13 --- /dev/null +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -0,0 +1,46 @@ +const mixins = { + methods: { + mouseOverDeployInfo(mouseXPos) { + if (!this.reducedDeploymentData) return false; + + let dataFound = false; + this.reducedDeploymentData = this.reducedDeploymentData.map((d) => { + const deployment = d; + if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { + dataFound = d.xPos + 1; + + deployment.showDeploymentFlag = true; + } else { + deployment.showDeploymentFlag = false; + } + return deployment; + }); + + return dataFound; + }, + formatDeployments() { + this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { + const time = new Date(deployment.created_at); + const xPos = Math.floor(this.xScale(time)); + + time.setSeconds(this.data[0].time.getSeconds()); + + if (xPos >= 0) { + deploymentDataArray.push({ + id: deployment.id, + time, + sha: deployment.sha, + tag: deployment.tag, + ref: deployment.ref.name, + xPos, + showDeploymentFlag: false, + }); + } + + return deploymentDataArray; + }, []); + }, + }, +}; + +export default mixins; diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index b3ce9310417..5d5cb56af72 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,6 +1,10 @@ -import PrometheusGraph from './prometheus_graph'; +import Vue from 'vue'; +import Monitoring from './components/monitoring.vue'; -document.addEventListener('DOMContentLoaded', function onLoad() { - document.removeEventListener('DOMContentLoaded', onLoad, false); - return new PrometheusGraph(); -}, false); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#prometheus-graphs', + components: { + 'monitoring-dashboard': Monitoring, + }, + render: createElement => createElement('monitoring-dashboard'), +})); diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js deleted file mode 100644 index 6af88769129..00000000000 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ /dev/null @@ -1,433 +0,0 @@ -/* eslint-disable no-new */ -/* global Flash */ - -import d3 from 'd3'; -import statusCodes from '~/lib/utils/http_status'; -import Deployments from './deployments'; -import '../lib/utils/common_utils'; -import { formatRelevantDigits } from '../lib/utils/number_utils'; -import '../flash'; -import { - dateFormat, - timeFormat, -} from './constants'; - -const prometheusContainer = '.prometheus-container'; -const prometheusParentGraphContainer = '.prometheus-graphs'; -const prometheusGraphsContainer = '.prometheus-graph'; -const prometheusStatesContainer = '.prometheus-state'; -const metricsEndpoint = 'metrics.json'; -const bisectDate = d3.bisector(d => d.time).left; -const extraAddedWidthParent = 100; - -class PrometheusGraph { - constructor() { - const $prometheusContainer = $(prometheusContainer); - const hasMetrics = $prometheusContainer.data('has-metrics'); - this.docLink = $prometheusContainer.data('doc-link'); - this.integrationLink = $prometheusContainer.data('prometheus-integration'); - this.state = ''; - - $(document).ajaxError(() => {}); - - if (hasMetrics) { - this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; - this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; - const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + - extraAddedWidthParent; - this.originalWidth = parentContainerWidth; - this.originalHeight = 330; - this.width = parentContainerWidth - this.margin.left - this.margin.right; - this.height = this.originalHeight - this.margin.top - this.margin.bottom; - this.backOffRequestCounter = 0; - this.deployments = new Deployments(this.width, this.height); - this.configureGraph(); - this.init(); - } else { - const prevState = this.state; - this.state = '.js-getting-started'; - this.updateState(prevState); - } - } - - createGraph() { - Object.keys(this.graphSpecificProperties).forEach((key) => { - const value = this.graphSpecificProperties[key]; - if (value.data.length > 0) { - this.plotValues(key); - } - }); - } - - init() { - return this.getData().then((metricsResponse) => { - let enoughData = true; - if (typeof metricsResponse === 'undefined') { - enoughData = false; - } else { - Object.keys(metricsResponse.metrics).forEach((key) => { - if (key === 'cpu_values' || key === 'memory_values') { - const currentData = (metricsResponse.metrics[key])[0]; - if (currentData.values.length <= 2) { - enoughData = false; - } - } - }); - } - if (enoughData) { - $(prometheusStatesContainer).hide(); - $(prometheusParentGraphContainer).show(); - this.transformData(metricsResponse); - this.createGraph(); - - const firstMetricData = this.graphSpecificProperties[ - Object.keys(this.graphSpecificProperties)[0] - ].data; - - this.deployments.init(firstMetricData); - } - }); - } - - plotValues(key) { - const graphSpecifics = this.graphSpecificProperties[key]; - - const x = d3.time.scale() - .range([0, this.width]); - - const y = d3.scale.linear() - .range([this.height, 0]); - - graphSpecifics.xScale = x; - graphSpecifics.yScale = y; - - const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; - - const chart = d3.select(prometheusGraphContainer) - .attr('width', this.width + this.margin.left + this.margin.right) - .attr('height', this.height + this.margin.bottom + this.margin.top) - .append('g') - .attr('class', 'graph-container') - .attr('transform', `translate(${this.margin.left},${this.margin.top})`); - - const axisLabelContainer = d3.select(prometheusGraphContainer) - .attr('width', this.originalWidth) - .attr('height', this.originalHeight) - .append('g') - .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); - - x.domain(d3.extent(graphSpecifics.data, d => d.time)); - y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]); - - const xAxis = d3.svg.axis() - .scale(x) - .ticks(this.commonGraphProperties.axis_no_ticks) - .orient('bottom'); - - const yAxis = d3.svg.axis() - .scale(y) - .ticks(this.commonGraphProperties.axis_no_ticks) - .tickSize(-this.width) - .outerTickSize(0) - .orient('left'); - - this.createAxisLabelContainers(axisLabelContainer, key); - - chart.append('g') - .attr('class', 'x-axis') - .attr('transform', `translate(0,${this.height})`) - .call(xAxis); - - chart.append('g') - .attr('class', 'y-axis') - .call(yAxis); - - const area = d3.svg.area() - .x(d => x(d.time)) - .y0(this.height) - .y1(d => y(d.value)) - .interpolate('linear'); - - const line = d3.svg.line() - .x(d => x(d.time)) - .y(d => y(d.value)); - - chart.append('path') - .datum(graphSpecifics.data) - .attr('d', area) - .attr('class', 'metric-area') - .attr('fill', graphSpecifics.area_fill_color); - - chart.append('path') - .datum(graphSpecifics.data) - .attr('class', 'metric-line') - .attr('stroke', graphSpecifics.line_color) - .attr('fill', 'none') - .attr('stroke-width', this.commonGraphProperties.area_stroke_width) - .attr('d', line); - - // Overlay area for the mouseover events - chart.append('rect') - .attr('class', 'prometheus-graph-overlay') - .attr('width', this.width) - .attr('height', this.height) - .on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer)); - } - - // The legends from the metric - createAxisLabelContainers(axisLabelContainer, key) { - const graphSpecifics = this.graphSpecificProperties[key]; - - axisLabelContainer.append('line') - .attr('class', 'label-x-axis-line') - .attr('stroke', '#000000') - .attr('stroke-width', '1') - .attr({ - x1: 10, - y1: this.originalHeight - this.margin.top, - x2: (this.originalWidth - this.margin.right) + 10, - y2: this.originalHeight - this.margin.top, - }); - - axisLabelContainer.append('line') - .attr('class', 'label-y-axis-line') - .attr('stroke', '#000000') - .attr('stroke-width', '1') - .attr({ - x1: 10, - y1: 0, - x2: 10, - y2: this.originalHeight - this.margin.top, - }); - - axisLabelContainer.append('rect') - .attr('class', 'rect-axis-text') - .attr('x', 0) - .attr('y', 50) - .attr('width', 30) - .attr('height', 150); - - axisLabelContainer.append('text') - .attr('class', 'label-axis-text') - .attr('text-anchor', 'middle') - .attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`) - .text(graphSpecifics.graph_legend_title); - - axisLabelContainer.append('rect') - .attr('class', 'rect-axis-text') - .attr('x', (this.originalWidth / 2) - this.margin.right) - .attr('y', this.originalHeight - 100) - .attr('width', 30) - .attr('height', 80); - - axisLabelContainer.append('text') - .attr('class', 'label-axis-text') - .attr('x', (this.originalWidth / 2) - this.margin.right) - .attr('y', this.originalHeight - this.margin.top) - .attr('dy', '.35em') - .text('Time'); - - // Legends - - // Metric Usage - axisLabelContainer.append('rect') - .attr('x', this.originalWidth - 170) - .attr('y', (this.originalHeight / 2) - 60) - .style('fill', graphSpecifics.area_fill_color) - .attr('width', 20) - .attr('height', 35); - - axisLabelContainer.append('text') - .attr('class', 'text-metric-title') - .attr('x', this.originalWidth - 140) - .attr('y', (this.originalHeight / 2) - 50) - .text('Average'); - - axisLabelContainer.append('text') - .attr('class', 'text-metric-usage') - .attr('x', this.originalWidth - 140) - .attr('y', (this.originalHeight / 2) - 25); - } - - handleMouseOverGraph(prometheusGraphContainer) { - const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); - const currentXCoordinate = d3.mouse(rectOverlay)[0]; - - Object.keys(this.graphSpecificProperties).forEach((key) => { - const currentGraphProps = this.graphSpecificProperties[key]; - const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate); - const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1); - const d0 = currentGraphProps.data[overlayIndex - 1]; - const d1 = currentGraphProps.data[overlayIndex]; - const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay; - const currentData = evalTime ? d1 : d0; - const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time)); - const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key); - const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; - const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value)); - const maxMetricValue = currentGraphProps.yScale(maxValueFromData); - - // Clear up all the pieces of the flag - d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove(); - d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove(); - d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove(); - - const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); - currentChart.append('line') - .attr({ - class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`, - x1: currentTimeCoordinate, - y1: currentGraphProps.yScale(0), - x2: currentTimeCoordinate, - y2: maxMetricValue, - }); - - currentChart.append('circle') - .attr('class', 'circle-metric') - .attr('fill', currentGraphProps.line_color) - .attr('cx', currentDeployXPos || currentTimeCoordinate) - .attr('cy', currentGraphProps.yScale(currentData.value)) - .attr('r', this.commonGraphProperties.circle_radius_metric); - - if (currentDeployXPos) return; - - // The little box with text - const rectTextMetric = currentChart.append('svg') - .attr({ - class: 'rect-text-metric', - x: currentTimeCoordinate, - y: 0, - }); - - rectTextMetric.append('rect') - .attr({ - class: 'rect-metric', - x: 4, - y: 1, - rx: 2, - width: this.commonGraphProperties.rect_text_width, - height: this.commonGraphProperties.rect_text_height, - }); - - rectTextMetric.append('text') - .attr({ - class: 'text-metric text-metric-bold', - x: 8, - y: 35, - }) - .text(timeFormat(currentData.time)); - - rectTextMetric.append('text') - .attr({ - class: 'text-metric-date', - x: 8, - y: 15, - }) - .text(dateFormat(currentData.time)); - - let currentMetricValue = formatRelevantDigits(currentData.value); - if (key === 'cpu_values') { - currentMetricValue = `${currentMetricValue}%`; - } else { - currentMetricValue = `${currentMetricValue} MB`; - } - - d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`) - .text(currentMetricValue); - }); - } - - configureGraph() { - this.graphSpecificProperties = { - cpu_values: { - area_fill_color: '#edf3fc', - line_color: '#5b99f7', - graph_legend_title: 'CPU Usage (Cores)', - data: [], - xScale: {}, - yScale: {}, - }, - memory_values: { - area_fill_color: '#fca326', - line_color: '#fc6d26', - graph_legend_title: 'Memory Usage (MB)', - data: [], - xScale: {}, - yScale: {}, - }, - }; - - this.commonGraphProperties = { - area_stroke_width: 2, - median_total_characters: 8, - circle_radius_metric: 5, - rect_text_width: 90, - rect_text_height: 40, - axis_no_ticks: 3, - }; - } - - getData() { - const maxNumberOfRequests = 3; - this.state = '.js-loading'; - this.updateState(); - return gl.utils.backOff((next, stop) => { - $.ajax({ - url: metricsEndpoint, - dataType: 'json', - }) - .done((data, statusText, resp) => { - if (resp.status === statusCodes.NO_CONTENT) { - this.backOffRequestCounter = this.backOffRequestCounter += 1; - if (this.backOffRequestCounter < maxNumberOfRequests) { - next(); - } else if (this.backOffRequestCounter >= maxNumberOfRequests) { - stop(new Error('loading')); - } - } else if (!data.success) { - stop(new Error('loading')); - } else { - stop({ - status: resp.status, - metrics: data, - }); - } - }).fail(stop); - }) - .then((resp) => { - if (resp.status === statusCodes.NO_CONTENT) { - return {}; - } - return resp.metrics; - }) - .catch(() => { - const prevState = this.state; - this.state = '.js-unable-to-connect'; - this.updateState(prevState); - }); - } - - transformData(metricsResponse) { - Object.keys(metricsResponse.metrics).forEach((key) => { - if (key === 'cpu_values' || key === 'memory_values') { - const metricValues = (metricsResponse.metrics[key])[0]; - this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ - time: new Date(metric[0] * 1000), - value: metric[1], - })); - } - }); - } - - updateState(prevState) { - const $statesContainer = $(prometheusStatesContainer); - $(prometheusParentGraphContainer).hide(); - if (prevState) { - $(`${prevState}`, $statesContainer).addClass('hidden'); - } - $(`${this.state}`, $statesContainer).removeClass('hidden'); - $(prometheusStatesContainer).show(); - } -} - -export default PrometheusGraph; diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js new file mode 100644 index 00000000000..1e9ae934853 --- /dev/null +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class MonitoringService { + constructor(endpoint) { + this.graphs = Vue.resource(endpoint); + } + + get() { + return this.graphs.get(); + } + + // eslint-disable-next-line class-methods-use-this + getDeploymentData(endpoint) { + return Vue.http.get(endpoint); + } +} diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js new file mode 100644 index 00000000000..737c964f12e --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -0,0 +1,61 @@ +import _ from 'underscore'; + +class MonitoringStore { + constructor() { + this.groups = []; + this.deploymentData = []; + } + + // eslint-disable-next-line class-methods-use-this + createArrayRows(metrics = []) { + const currentMetrics = metrics; + const availableMetrics = []; + let metricsRow = []; + let index = 1; + Object.keys(currentMetrics).forEach((key) => { + const metricValues = currentMetrics[key].queries[0].result[0].values; + if (metricValues != null) { + const literalMetrics = metricValues.map(metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); + currentMetrics[key].queries[0].result[0].values = literalMetrics; + metricsRow.push(currentMetrics[key]); + if (index % 2 === 0) { + availableMetrics.push(metricsRow); + metricsRow = []; + } + index = index += 1; + } + }); + if (metricsRow.length > 0) { + availableMetrics.push(metricsRow); + } + return availableMetrics; + } + + storeMetrics(groups = []) { + this.groups = groups.map((group) => { + const currentGroup = group; + currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value(); + currentGroup.metrics = this.createArrayRows(currentGroup.metrics); + return currentGroup; + }); + } + + storeDeploymentData(deploymentData = []) { + this.deploymentData = deploymentData; + } + + getMetricsCount() { + let metricsCount = 0; + this.groups.forEach((group) => { + group.metrics.forEach((metric) => { + metricsCount = metricsCount += metric.length; + }); + }); + return metricsCount; + } +} + +export default MonitoringStore; diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js new file mode 100644 index 00000000000..a60d2522f49 --- /dev/null +++ b/app/assets/javascripts/monitoring/utils/measurements.js @@ -0,0 +1,39 @@ +export default { + small: { // Covers both xs and sm screen sizes + margin: { + top: 40, + right: 40, + bottom: 50, + left: 40, + }, + legends: { + width: 15, + height: 30, + }, + backgroundLegend: { + width: 30, + height: 50, + }, + axisLabelLineOffset: -20, + legendOffset: 52, + }, + large: { // This covers both md and lg screen sizes + margin: { + top: 80, + right: 80, + bottom: 100, + left: 80, + }, + legends: { + width: 20, + height: 35, + }, + backgroundLegend: { + width: 30, + height: 150, + }, + axisLabelLineOffset: 20, + legendOffset: 55, + }, + ticks: 3, +}; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b21d7774920..46d77b31ffd 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -829,6 +829,8 @@ export default class Notes { */ setupDiscussionNoteForm(dataHolder, form) { // setup note target + const diffFileData = dataHolder.closest('.text-file'); + var discussionID = dataHolder.data('discussionId'); if (discussionID) { @@ -839,9 +841,10 @@ export default class Notes { 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_noteable_type').val(diffFileData.data('noteableType')); + form.find('#note_noteable_id').val(diffFileData.data('noteableId')); + form.find('#note_commit_id').val(diffFileData.data('commitId')); + form.find('#note_type').val(dataHolder.data('noteType')); // LegacyDiffNote @@ -1485,7 +1488,7 @@ export default class Notes { const cachedNoteBodyText = $noteBodyText.html(); // Show updated comment content temporarily - $noteBodyText.html(_.escape(formContent)); + $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>'); 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 901adbe9fce..00000000000 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js +++ /dev/null @@ -1,148 +0,0 @@ -import Vue from 'vue'; -import Translate from '../../vue_shared/translate'; - -Vue.use(Translate); - -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"> - {{ 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> - `, -}; 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/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 abcd0c4ecea..16cc0761fc1 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -3,7 +3,7 @@ import eventHub from '../event_hub'; 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: { @@ -28,12 +28,12 @@ export default { required: false, }, }, + directives: { + tooltip, + }, components: { loadingIcon, }, - mixins: [ - tooltipMixin, - ], data() { return { isLoading: false, @@ -58,7 +58,6 @@ export default { makeRequest() { this.isLoading = true; - $(this.$refs.tooltip).tooltip('destroy'); eventHub.$emit('postAction', this.endpoint); }, }, @@ -67,6 +66,7 @@ export default { <template> <button + v-tooltip type="button" @click="onClick" :class="buttonClass" @@ -74,7 +74,6 @@ export default { :aria-label="title" data-container="body" data-placement="top" - ref="tooltip" :disabled="isLoading"> <i :class="iconClass" 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 8333ec0fbc3..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; @@ -45,16 +45,16 @@ export default { <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" - ref="tooltip"> + 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" - ref="tooltip"> + :title="pipeline.yaml_errors"> yaml invalid </span> <span diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index a6fc4f04237..01dfe51cc17 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -4,6 +4,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,9 @@ required: true, }, }, + directives: { + tooltip, + }, components: { loadingIcon, }, @@ -25,8 +29,6 @@ onClickAction(endpoint) { this.isLoading = true; - $(this.$refs.tooltip).tooltip('destroy'); - eventHub.$emit('postAction', endpoint); }, @@ -43,13 +45,13 @@ <template> <div class="btn-group"> <button + v-tooltip type="button" - class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" + class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" title="Manual job" data-toggle="dropdown" data-placement="top" aria-label="Manual job" - ref="tooltip" :disabled="isLoading"> <span v-html="playIconSvg"></span> <i diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index b4520481cdc..b19bd509a00 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,5 +1,5 @@ <script> - import tooltipMixin from '../../vue_shared/mixins/tooltip'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -8,9 +8,9 @@ required: true, }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, }; </script> <template> @@ -18,12 +18,12 @@ 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" - ref="tooltip"> + aria-label="Artifacts"> <i class="fa fa-download" aria-hidden="true"> 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.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index be3f32afa09..037684b4e72 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -1,7 +1,7 @@ <script> import iconTimerSvg from 'icons/_icon_timer.svg'; import '../../lib/utils/datetime_utility'; - import tooltipMixin from '../../vue_shared/mixins/tooltip'; + import tooltip from '../../vue_shared/directives/tooltip'; import timeagoMixin from '../../vue_shared/mixins/timeago'; export default { @@ -16,9 +16,11 @@ }, }, mixins: [ - tooltipMixin, timeagoMixin, ], + directives: { + tooltip, + }, data() { return { iconTimerSvg, @@ -81,7 +83,7 @@ </i> <time - ref="tooltip" + v-tooltip data-placement="top" data-container="body" :title="tooltipTitle(finishedTime)"> diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 322162afdb8..b5cd01044a3 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -10,6 +10,8 @@ import Cookies from 'js-cookie'; this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); this.$navGitlab = $('.navbar-gitlab'); + this.$layoutNav = $('.layout-nav'); + this.$subScroll = $('.sub-nav-scroll'); this.$rightSidebar = $('.js-right-sidebar'); this.removeListeners(); @@ -27,14 +29,14 @@ import Cookies from 'js-cookie'; Sidebar.prototype.addEventListeners = function() { const $document = $(document); const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); - const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200); + const slowerThrottledSetSidebarHeight = _.throttle(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', () => debouncedSetSidebarHeight()); + $document.on('scroll', () => slowerThrottledSetSidebarHeight()); $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -213,7 +215,7 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = this.$navGitlab.outerHeight(); + const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { this.$rightSidebar.outerHeight($(window).height() - diff); 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/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index c44892dae3d..9316a2af0b7 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ +import FilesCommentButton from './files_comment_button'; + (function() { window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; @@ -78,6 +80,8 @@ gl.diffNotesCompileComponents(); } + FilesCommentButton.init($(_this.file)); + if (cb) cb(); }; })(this)); 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/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/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index e6977681e96..8303c556f64 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -64,6 +64,12 @@ */ return new gl.GLForm($(this.$refs['gl-form']), true); }, + beforeDestroy() { + const glForm = $(this.$refs['gl-form']).data('gl-form'); + if (glForm) { + glForm.destroy(); + } + }, }; </script> 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/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/files.scss b/app/assets/stylesheets/framework/files.scss index 31220ab438e..245117b2559 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -129,7 +129,7 @@ } /** - * Annotate file + * Blame file */ &.blame { table { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 94986badaf9..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,7 @@ header { width: auto; min-width: 140px; margin-top: -5px; + color: $gl-text-color; left: auto; .current-user { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 476427c1593..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); diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss new file mode 100644 index 00000000000..3ce5b4fd073 --- /dev/null +++ b/app/assets/stylesheets/new_nav.scss @@ -0,0 +1,266 @@ +@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 { + align-items: stretch; + padding-top: 0; + overflow: visible; + } + + .title { + display: flex; + padding-right: 0; + color: currentColor; + + > a { + display: flex; + align-items: center; + 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..be4cc02b3ea --- /dev/null +++ b/app/assets/stylesheets/new_sidebar.scss @@ -0,0 +1,150 @@ +@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; + } + + // Override position: absolute + .right-sidebar { + position: fixed; + height: 100%; + } +} + +.context-header { + background-color: $gray-normal; + border-bottom: 1px solid $border-color; + font-weight: 600; + display: flex; + align-items: center; + padding: 10px 14px; + + .avatar-container { + flex: 0 0 40px; + } + + &:hover { + background-color: $border-color; + } +} + +.settings-avatar { + background-color: $white-light; + + i { + font-size: 20px; + width: 100%; + color: $gl-text-color-secondary; + text-align: center; + align-self: center; + } +} + +.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/builds.scss b/app/assets/stylesheets/pages/builds.scss index 7eee0a71c66..9cff99b839c 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -147,10 +147,9 @@ top: 35px; left: 10px; bottom: 0; - overflow-y: scroll; - overflow-x: hidden; padding: 10px 20px 20px 5px; - white-space: pre; + white-space: pre-wrap; + overflow: auto; } .environment-information { @@ -399,6 +398,7 @@ .build-light-text { color: $gl-text-color-secondary; + word-wrap: break-word; } .build-gutter-toggle { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b58922626fa..631649b363f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -476,6 +476,7 @@ height: 19px; width: 19px; margin-left: -15px; + z-index: 100; &:hover { .diff-comment-avatar, @@ -491,7 +492,7 @@ transform: translateX((($i * $x-pos) - $x-pos)); &:hover { - transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + transform: translateX((($i * $x-pos) - $x-pos)); } } } @@ -542,6 +543,7 @@ height: 19px; padding: 0; transition: transform .1s ease-out; + z-index: 100; svg { position: absolute; @@ -555,10 +557,6 @@ fill: $white-light; } - &:hover { - transform: scale(1.2); - } - &:focus { outline: 0; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 1046ebfa2e2..a2be957655f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -140,23 +140,6 @@ } } -.prometheus-graph { - text { - fill: $gl-text-color; - stroke-width: 0; - } - - .label-axis-text, - .text-metric-usage { - fill: $black; - font-weight: 500; - } - - .legend-axis-text { - fill: $black; - } -} - .x-axis path, .y-axis path, .label-x-axis-line, @@ -205,6 +188,7 @@ .text-metric { font-weight: 600; + font-size: 14px; } .selected-metric-line { @@ -214,20 +198,15 @@ .deployment-line { stroke: $black; - stroke-width: 2; + stroke-width: 1; } .deploy-info-text { dominant-baseline: text-before-edge; } -.text-metric-bold { - font-weight: 600; -} - .prometheus-state { margin-top: 10px; - display: none; .state-button-section { margin-top: 10px; @@ -242,3 +221,59 @@ width: 38px; } } + +.prometheus-panel { + margin-top: 20px; +} + +.prometheus-svg-container { + position: relative; + height: 0; + width: 100%; + padding: 0; + padding-bottom: 100%; + + .text-metric-bold { + font-weight: 600; + } +} + +.prometheus-svg-container > svg { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + + text { + fill: $gl-text-color; + stroke-width: 0; + } + + .label-axis-text, + .text-metric-usage { + fill: $black; + font-weight: 500; + font-size: 14px; + } + + .legend-axis-text { + fill: $black; + } + + .tick > text { + font-size: 14px; + } + + @media (max-width: $screen-sm-max) { + .label-axis-text, + .text-metric-usage, + .legend-axis-text { + font-size: 8px; + } + + .tick > text { + font-size: 8px; + } + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0d1a360d12c..057d457b3a2 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -228,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; } @@ -249,6 +255,10 @@ border-left: 1px solid $border-gray-normal; } + .title .gutter-toggle { + margin-top: 0; + } + .assignee .avatar { float: left; margin-right: 10px; @@ -587,7 +597,38 @@ .issue-info-container { -webkit-flex: 1; flex: 1; + display: flex; padding-right: $gl-padding; + + .issue-main-info { + flex: 1 auto; + margin-right: 10px; + } + + .issuable-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + flex: 1 0 auto; + + .controls { + margin-bottom: 2px; + line-height: 20px; + padding: 0; + } + + .issue-updated-at { + line-height: 20px; + } + } + + @media(max-width: $screen-xs-max) { + .issuable-meta { + .controls li { + margin-right: 0; + } + } + } } .issue-check { @@ -599,6 +640,30 @@ vertical-align: text-top; } } + + .issuable-milestone, + .issuable-info, + .task-status, + .issuable-updated-at { + font-weight: normal; + color: $gl-text-color-secondary; + + a { + color: $gl-text-color; + + .fa { + color: $gl-text-color-secondary; + } + } + } + + @media(max-width: $screen-md-max) { + .task-status, + .issuable-due-date, + .project-ref-path { + display: none; + } + } } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index c10588ac58e..ee48f7a3626 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 { @@ -278,5 +279,9 @@ .label-link { display: inline-block; - vertical-align: text-top; + vertical-align: top; + + .label { + vertical-align: inherit; + } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index f0dbe4249c5..303425041df 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -473,7 +473,7 @@ ul.notes { } .more-actions { - display: inline; + display: inline-block; .tooltip { white-space: nowrap; @@ -628,8 +628,14 @@ ul.notes { * Line note button on the side of diffs */ +.line_holder .is-over:not(.no-comment-btn) { + .add-diff-note { + opacity: 1; + } +} + .add-diff-note { - display: none; + opacity: 0; margin-top: -2px; border-radius: 50%; background: $white-light; @@ -642,13 +648,11 @@ ul.notes { width: 23px; height: 23px; border: 1px solid $blue-500; - transition: transform .1s ease-in-out; &:hover { background: $blue-500; border-color: $blue-600; color: $white-light; - transform: scale(1.15); } &:active { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 562ecbc6986..ba530bf7f9b 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -377,6 +377,7 @@ a.deploy-project-label { } .breadcrumb.repo-breadcrumb { + flex: 1; padding: 0; background: transparent; border: none; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index ce1a13c6afa..dc88cf3e699 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,4 +1,5 @@ .tree-holder { + .nav-block { margin: 10px 0; @@ -15,6 +16,11 @@ .btn-group { margin-left: 10px; } + + .control { + float: left; + margin-left: 10px; + } } .tree-ref-holder { @@ -70,7 +76,8 @@ } .file-finder { - width: 50%; + max-width: 500px; + width: 100%; .file-finder-input { width: 95%; diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 2eac0cabf7a..ed13ead63f9 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -1,7 +1,9 @@ class AbuseReportsController < ApplicationController + before_action :set_user, only: [:new] + def new @abuse_report = AbuseReport.new - @abuse_report.user_id = params[:user_id] + @abuse_report.user_id = @user.id @ref_url = params.fetch(:ref_url, '') end @@ -27,4 +29,14 @@ class AbuseReportsController < ApplicationController user_id )) end + + def set_user + @user = User.find_by(id: params[:user_id]) + + if @user.nil? + redirect_to root_path, alert: "Cannot create the abuse report. The user has been deleted." + elsif @user.blocked? + redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked." + end + end end 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/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 1a9904bbe57..f87db4d9e84 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -78,7 +78,7 @@ module CreatesCommit end def new_merge_request_path - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( @project_to_commit_into.namespace, @project_to_commit_into, merge_request: { 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 f98a9e24de1..076076fd1b3 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -12,39 +12,47 @@ 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 @@ -55,12 +63,13 @@ class ProfilesController < Profiles::ApplicationController 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/issues_controller.rb b/app/controllers/projects/issues_controller.rb index dfc6baa34a4..ca483c105b6 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -267,10 +267,22 @@ class Projects::IssuesController < Projects::ApplicationController end def issue_params - params.require(:issue).permit( - :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: [] - ) + params.require(:issue).permit(*issue_params_attributes) + end + + def issue_params_attributes + %i[ + title + assignee_id + position + description + confidential + milestone_id + due_date + state_event + task_num + lock_version + ] + [{ label_ids: [], assignee_ids: [] }] end def authenticate_user! diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb new file mode 100644 index 00000000000..5de0f828010 --- /dev/null +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -0,0 +1,48 @@ +class Projects::MergeRequests::ApplicationController < Projects::ApplicationController + before_action :check_merge_requests_available! + before_action :merge_request + before_action :authorize_read_merge_request! + before_action :ensure_ref_fetched + + private + + def merge_request + @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) + end + + # Make sure merge requests created before 8.0 + # have head file in refs/merge-requests/ + def ensure_ref_fetched + @merge_request.ensure_ref_fetched + end + + def merge_request_params + params.require(:merge_request) + .permit(merge_request_params_attributes) + end + + def merge_request_params_attributes + [ + :assignee_id, + :description, + :force_remove_source_branch, + :lock_version, + :milestone_id, + :source_branch, + :source_project_id, + :state_event, + :target_branch, + :target_project_id, + :task_num, + :title, + + label_ids: [] + ] + end + + def set_pipeline_variables + @pipelines = @merge_request.all_pipelines + @pipeline = @merge_request.head_pipeline + @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 + end +end diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb new file mode 100644 index 00000000000..a71f23e790d --- /dev/null +++ b/app/controllers/projects/merge_requests/conflicts_controller.rb @@ -0,0 +1,66 @@ +class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::ApplicationController + include IssuableActions + + before_action :authorize_can_resolve_conflicts! + + def show + respond_to do |format| + format.html do + labels + end + + format.json do + if @conflicts_list.can_be_resolved_in_ui? + render json: @conflicts_list + elsif @merge_request.can_be_merged? + render json: { + message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.', + type: 'error' + } + else + render json: { + message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.', + type: 'error' + } + end + end + end + end + + def conflict_for_path + return render_404 unless @conflicts_list.can_be_resolved_in_ui? + + file = @conflicts_list.file_for_path(params[:old_path], params[:new_path]) + + return render_404 unless file + + render json: file, full_content: true + end + + def resolve_conflicts + return render_404 unless @conflicts_list.can_be_resolved_in_ui? + + if @merge_request.can_be_merged? + render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' } + return + end + + begin + ::MergeRequests::Conflicts::ResolveService + .new(merge_request) + .execute(current_user, params) + + flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' + + render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } + rescue Gitlab::Conflict::ResolutionError => e + render status: :bad_request, json: { message: e.message } + end + end + + def authorize_can_resolve_conflicts! + @conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request) + + return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) + end +end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb new file mode 100644 index 00000000000..da058da795e --- /dev/null +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -0,0 +1,128 @@ +class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController + include DiffForPath + include DiffHelper + + skip_before_action :merge_request + skip_before_action :ensure_ref_fetched + before_action :authorize_create_merge_request! + before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] + before_action :build_merge_request, except: [:create] + + def new + define_new_vars + end + + def create + @target_branches ||= [] + @merge_request = ::MergeRequests::CreateService.new(project, current_user, merge_request_params).execute + + if @merge_request.valid? + redirect_to(merge_request_path(@merge_request)) + else + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + + define_new_vars + render action: "new" + end + end + + def pipelines + @pipelines = @merge_request.all_pipelines + + Gitlab::PollingInterval.set_header(response, interval: 10_000) + + render json: { + pipelines: PipelineSerializer + .new(project: @project, current_user: @current_user) + .represent(@pipelines) + } + end + + def diffs + @diffs = if @merge_request.can_be_created + @merge_request.diffs(diff_options) + else + [] + end + @diff_notes_disabled = true + + @environment = @merge_request.environments_for(current_user).last + + render json: { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs, environment: @environment) } + end + + def diff_for_path + @diffs = @merge_request.diffs(diff_options) + @diff_notes_disabled = true + + render_diff_for_path(@diffs) + end + + def branch_from + # This is always source + @source_project = @merge_request.nil? ? @project : @merge_request.source_project + + if params[:ref].present? + @ref = params[:ref] + @commit = @repository.commit("refs/heads/#{@ref}") + end + + render layout: false + end + + def branch_to + @target_project = selected_target_project + + if params[:ref].present? + @ref = params[:ref] + @commit = @target_project.commit("refs/heads/#{@ref}") + end + + render layout: false + end + + def update_branches + @target_project = selected_target_project + @target_branches = @target_project.repository.branch_names + + render layout: false + end + + private + + def build_merge_request + params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) + @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute + end + + def define_new_vars + @noteable = @merge_request + + @target_branches = if @merge_request.target_project + @merge_request.target_project.repository.branch_names + else + [] + end + + @target_project = @merge_request.target_project + @source_project = @merge_request.source_project + @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 + + @labels = LabelsFinder.new(current_user, project_id: @project.id).execute + + set_pipeline_variables + end + + def selected_target_project + if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? + @project + else + @project.forked_project_link.forked_from_project + end + end +end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb new file mode 100644 index 00000000000..330b7df4541 --- /dev/null +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -0,0 +1,66 @@ +class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController + include DiffForPath + include DiffHelper + include RendersNotes + + before_action :apply_diff_view_cookie! + before_action :define_diff_vars + before_action :define_diff_comment_vars + + def show + @environment = @merge_request.environments_for(current_user).last + + render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } + end + + def diff_for_path + render_diff_for_path(@diffs) + end + + private + + def define_diff_vars + @merge_request_diff = + if params[:diff_id] + @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) + else + @merge_request.merge_request_diff + end + + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff + @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } + + if params[:start_sha].present? + @start_sha = params[:start_sha] + @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } + + unless @start_version + @start_sha = @merge_request_diff.head_commit_sha + @start_version = @merge_request_diff + end + end + + @compare = + if @start_sha + @merge_request_diff.compare_with(@start_sha) + else + @merge_request_diff + end + + @diffs = @compare.diffs(diff_options) + end + + def define_diff_comment_vars + @new_diff_note_attrs = { + noteable_type: 'MergeRequest', + noteable_id: @merge_request.id + } + + @diff_notes_disabled = false + + @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? + + @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs) + @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes)) + end +end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 879ff6d393e..04f8e95aa09 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,38 +1,17 @@ -class Projects::MergeRequestsController < Projects::ApplicationController +class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationController include ToggleSubscriptionAction - include DiffForPath - include DiffHelper include IssuableActions include RendersNotes include ToggleAwardEmoji include IssuableCollections - 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 - ] - before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] - before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] - before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] - before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] - before_action :check_if_can_be_merged, only: :show - before_action :apply_diff_view_cookie!, only: [:new_diffs] - before_action :build_merge_request, only: [:new, :new_diffs] - - # Allow read any merge_request - before_action :authorize_read_merge_request! - - # Allow write(create) merge_request - before_action :authorize_create_merge_request!, only: [:new, :create] - - # Allow modify merge_request + skip_before_action :merge_request, only: [:index, :bulk_update] + skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update] + before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authenticate_user!, only: [:assign_related_issues] - before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts] - def index @collection_type = "MergeRequest" @merge_requests = merge_requests_collection @@ -72,10 +51,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def show + validates_merge_request + ensure_ref_fetched + close_merge_request_without_source_project + check_if_can_be_merged + respond_to do |format| format.html do - define_discussion_vars - define_show_vars + # Build a note object for comment form + @note = @project.notes.new(noteable: @merge_request) + + @discussions = @merge_request.discussions + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + + @noteable = @merge_request + @commits_count = @merge_request.commits_count + + if @merge_request.locked_long_ago? + @merge_request.unlock_mr + @merge_request.close + end + + labels + + set_pipeline_variables end format.json do @@ -98,198 +97,40 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def diffs - apply_diff_view_cookie! - - respond_to do |format| - format.html { define_discussion_vars } - format.json do - define_diff_vars - define_diff_comment_vars - - @environment = @merge_request.environments_for(current_user).last - - render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } - end - end - end - - # With an ID param, loads the MR at that ID. Otherwise, accepts the same params as #new - # and uses that (unsaved) MR. - # - def diff_for_path - if params[:id] - merge_request - define_diff_vars - define_diff_comment_vars - else - build_merge_request - @compare = @merge_request - @diffs = @compare.diffs(diff_options) - @diff_notes_disabled = true - end - - render_diff_for_path(@diffs) - end - def commits - respond_to do |format| - format.html do - define_discussion_vars - - render 'show' - end - format.json do - # 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 - - render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } - end - end - end - - def conflicts - respond_to do |format| - format.html { define_discussion_vars } - - format.json do - if @conflicts_list.can_be_resolved_in_ui? - render json: @conflicts_list - elsif @merge_request.can_be_merged? - render json: { - message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.', - type: 'error' - } - else - render json: { - message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.', - type: 'error' - } - end - end - end - end - - def conflict_for_path - return render_404 unless @conflicts_list.can_be_resolved_in_ui? - - file = @conflicts_list.file_for_path(params[:old_path], params[:new_path]) - - return render_404 unless file - - render json: file, full_content: true - end - - def resolve_conflicts - return render_404 unless @conflicts_list.can_be_resolved_in_ui? - - if @merge_request.can_be_merged? - render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' } - return - end - - begin - MergeRequests::Conflicts::ResolveService - .new(merge_request) - .execute(current_user, params) - - flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' + # 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 - render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } - rescue Gitlab::Conflict::ResolutionError => e - render status: :bad_request, json: { message: e.message } - end + render json: { html: view_to_html_string('projects/merge_requests/_commits') } end def pipelines @pipelines = @merge_request.all_pipelines - respond_to do |format| - format.html do - define_discussion_vars - - render 'show' - end - - format.json do - Gitlab::PollingInterval.set_header(response, interval: 10_000) + Gitlab::PollingInterval.set_header(response, interval: 10_000) - render json: PipelineSerializer - .new(project: @project, current_user: @current_user) - .represent(@pipelines) - end - end - end - - def new - respond_to do |format| - format.html { define_new_vars } - format.json do - define_pipelines_vars - - Gitlab::PollingInterval.set_header(response, interval: 10_000) - - render json: { - pipelines: PipelineSerializer - .new(project: @project, current_user: @current_user) - .represent(@pipelines) - } - end - end - end - - def new_diffs - respond_to do |format| - format.html do - define_new_vars - @show_changes_tab = true - render "new" - end - format.json do - @diffs = if @merge_request.can_be_created - @merge_request.diffs(diff_options) - else - [] - end - @diff_notes_disabled = true - - @environment = @merge_request.environments_for(current_user).last - - render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) } - end - end - end - - def create - @target_branches ||= [] - @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute - - if @merge_request.valid? - redirect_to(merge_request_path(@merge_request)) - else - @source_project = @merge_request.source_project - @target_project = @merge_request.target_project - render action: "new" - end + render json: PipelineSerializer + .new(project: @project, current_user: @current_user) + .represent(@pipelines) end def edit - @source_project = @merge_request.source_project - @target_project = @merge_request.target_project - @target_branches = @merge_request.target_project.repository.branch_names + define_edit_vars end def update - @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) + @merge_request = ::MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) respond_to do |format| format.html do if @merge_request.valid? redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request]) else + define_edit_vars + render :edit end end @@ -299,11 +140,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end rescue ActiveRecord::StaleObjectError + define_edit_vars if request.format.html? + render_conflict_response end def remove_wip - @merge_request = MergeRequests::UpdateService + @merge_request = ::MergeRequests::UpdateService .new(project, current_user, wip_event: 'unwip') .execute(@merge_request) @@ -319,7 +162,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController return access_denied! end - MergeRequests::MergeWhenPipelineSucceedsService + ::MergeRequests::MergeWhenPipelineSucceedsService .new(@project, current_user) .cancel(@merge_request) @@ -338,53 +181,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def branch_from - # This is always source - @source_project = @merge_request.nil? ? @project : @merge_request.source_project - - if params[:ref].present? - @ref = params[:ref] - @commit = @repository.commit("refs/heads/#{@ref}") - end - - render layout: false - end - - def branch_to - @target_project = selected_target_project - - if params[:ref].present? - @ref = params[:ref] - @commit = @target_project.commit("refs/heads/#{@ref}") - end - - render layout: false - end - - def update_branches - @target_project = selected_target_project - @target_branches = @target_project.repository.branch_names - - render layout: false - end - def assign_related_issues - result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute + result = ::MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute - respond_to do |format| - format.html do - case result[:count] - when 0 - flash[:error] = "Failed to assign you issues related to the merge request" - when 1 - flash[:notice] = "1 issue has been assigned to you" - else - flash[:notice] = "#{result[:count]} issues have been assigned to you" - end - - redirect_to(merge_request_path(@merge_request)) - end + case result[:count] + when 0 + flash[:error] = "Failed to assign you issues related to the merge request" + when 1 + flash[:notice] = "1 issue has been assigned to you" + else + flash[:notice] = "#{result[:count]} issues have been assigned to you" end + + redirect_to(merge_request_path(@merge_request)) end def pipeline_status @@ -432,17 +241,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController protected - def selected_target_project - if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? - @project - else - @project.forked_project_link.forked_from_project - end - end - - def merge_request - @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) - end alias_method :subscribable_resource, :merge_request alias_method :issuable, :merge_request alias_method :awardable, :merge_request @@ -455,12 +253,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController return render_404 unless can?(current_user, :admin_merge_request, @merge_request) end - def authorize_can_resolve_conflicts! - @conflicts_list = MergeRequests::Conflicts::ListService.new(@merge_request) - - return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) - end - def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch @@ -470,133 +262,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def define_show_vars - @noteable = @merge_request - @commits_count = @merge_request.commits_count - - if @merge_request.locked_long_ago? - @merge_request.unlock_mr - @merge_request.close - end - - labels - define_pipelines_vars - end - - # Discussion tab data is rendered on html responses of actions - # :show, :diff, :commits, :builds. but not when request the data through AJAX - def define_discussion_vars - # Build a note object for comment form - @note = @project.notes.new(noteable: @merge_request) - - @discussions = @merge_request.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) - end - - def define_diff_vars - @merge_request_diff = - if params[:diff_id] - @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) - else - @merge_request.merge_request_diff - end - - @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff - @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } - - if params[:start_sha].present? - @start_sha = params[:start_sha] - @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } - - unless @start_version - @start_sha = @merge_request_diff.head_commit_sha - @start_version = @merge_request_diff - end - end - - @compare = - if @start_sha - @merge_request_diff.compare_with(@start_sha) - else - @merge_request_diff - end - - @diffs = @compare.diffs(diff_options) - end - - def define_diff_comment_vars - @new_diff_note_attrs = { - noteable_type: 'MergeRequest', - noteable_id: @merge_request.id - } - - @diff_notes_disabled = false - - @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? - - @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs) - @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes)) - end - - def define_pipelines_vars - @pipelines = @merge_request.all_pipelines - @pipeline = @merge_request.head_pipeline - @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 - end - - def define_new_vars - @noteable = @merge_request - - @target_branches = if @merge_request.target_project - @merge_request.target_project.repository.branch_names - else - [] - end - - @target_project = merge_request.target_project - @source_project = merge_request.source_project - @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 - - @labels = LabelsFinder.new(current_user, project_id: @project.id).execute - - @show_changes_tab = params[:show_changes].present? - - define_pipelines_vars - end - def invalid_mr # Render special view for MR with removed target branch render 'invalid' end - def merge_request_params - params.require(:merge_request) - .permit(merge_request_params_attributes) - end - - def merge_request_params_attributes - [ - :assignee_id, - :description, - :force_remove_source_branch, - :lock_version, - :milestone_id, - :source_branch, - :source_project_id, - :state_event, - :target_branch, - :target_project_id, - :task_num, - :title, - - label_ids: [] - ] - end - def merge_params params.permit(merge_params_attributes) end @@ -605,22 +275,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController [:should_remove_source_branch, :commit_message] end - # Make sure merge requests created before 8.0 - # have head file in refs/merge-requests/ - def ensure_ref_fetched - @merge_request.ensure_ref_fetched - end - def merge_when_pipeline_succeeds_active? params[:merge_when_pipeline_succeeds].present? && @merge_request.head_pipeline && @merge_request.head_pipeline.active? end - def build_merge_request - params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) - @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute - end - def close_merge_request_without_source_project if !@merge_request.source_project && @merge_request.open? @merge_request.close @@ -648,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController return :failed unless @merge_request.head_pipeline if @merge_request.head_pipeline.active? - MergeRequests::MergeWhenPipelineSucceedsService + ::MergeRequests::MergeWhenPipelineSucceedsService .new(@project, current_user, merge_params) .execute(@merge_request) @@ -672,4 +331,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController def serializer MergeRequestSerializer.new(current_user: current_user, project: merge_request.project) end + + def define_edit_vars + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + @target_branches = @merge_request.target_project.repository.branch_names + end end diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index ef4f083b98f..60db179277b 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -1,6 +1,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController before_action :authorize_read_pipeline_schedule! - before_action :authorize_create_pipeline_schedule!, only: [:new, :create, :edit, :take_ownership, :update] + before_action :authorize_create_pipeline_schedule!, only: [:new, :create] + before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update] before_action :authorize_admin_pipeline_schedule!, only: [:destroy] before_action :schedule, only: [:edit, :update, :destroy, :take_ownership] diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 8effb792689..303e91a8dc0 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -135,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_controller.rb b/app/controllers/projects_controller.rb index 5480814874b..450895cdf3a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController end if @project.pending_delete? - flash[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } + flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } end respond_to do |format| diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 0d8186dce02..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 diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index c358f23f541..3fe37c75381 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -83,6 +83,8 @@ class TodosFinder if project? @project = Project.find(params[:project_id]) + @project = nil if @project.pending_delete? + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a3b243fccb7..7be8e3b96cf 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -131,10 +131,7 @@ module ApplicationHelper end def body_data_page - path = controller.controller_path.split('/') - namespace = path.first if path.second - - [namespace, controller.controller_name, controller.action_name].compact.join(':') + [*controller.controller_path.split('/'), controller.action_name].compact.join(':') end # shortcut for gitlab config @@ -300,4 +297,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/blob_helper.rb b/app/helpers/blob_helper.rb index 3efa7c36057..ee36617ba9a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -284,7 +284,7 @@ module BlobHelper merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project)) if merge_project - options << link_to("create a merge request", new_namespace_project_merge_request_path(project.namespace, project)) + options << link_to("create a merge request", namespace_project_new_merge_request_path(project.namespace, project)) end options 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/compare_helper.rb b/app/helpers/compare_helper.rb index 2aa0449c46e..424ded2b69d 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -9,7 +9,7 @@ module CompareHelper end def create_mr_path(from = params[:from], to = params[:to], project = @project) - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( project.namespace, project, merge_request: { 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/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/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 39d30631646..54d6f86fa11 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,7 +1,7 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) target_project = event.project.default_merge_request_target - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( event.project.namespace, event.project, new_mr_from_push_event(event, target_project) @@ -48,7 +48,7 @@ module MergeRequestsHelper end def mr_change_branches_path(merge_request) - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( @project.namespace, @project, merge_request: { source_project_id: merge_request.source_project_id, diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 833d3c36b28..e589ed4e56d 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,11 +1,7 @@ module NavHelper def page_gutter_class if current_path?('merge_requests#show') || - current_path?('merge_requests#diffs') || - current_path?('merge_requests#commits') || - current_path?('merge_requests#builds') || - current_path?('merge_requests#conflicts') || - current_path?('merge_requests#pipelines') || + current_path?('projects/merge_requests/conflicts#show') || current_path?('issues#show') || current_path?('milestones#show') if cookies[:collapsed_gutter] == 'true' diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 64ad7b280cb..ecc6cd6c6c5 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -47,6 +47,18 @@ module NotesHelper data end + def add_diff_note_button(line_code, position, line_type) + return if @diff_notes_disabled + + button_tag '', + class: 'add-diff-note js-add-diff-note-button', + type: 'submit', name: 'button', + data: diff_view_line_data(line_code, position, line_type), + title: 'Add a comment to this line' do + icon('comment-o') + end + end + def link_to_reply_discussion(discussion, line_type = nil) return unless current_user diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d10e0bd45b0..c04b1419a19 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -198,6 +198,23 @@ module ProjectsHelper .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 def repo_children_classes(field) diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 8e0a1e2ecdf..b24039fb349 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -73,6 +73,7 @@ module SubmoduleHelper end def relative_self_links(url, commit) + url.rstrip! # Map relative links to a namespace and project # For example: # ../bar.git -> same namespace, repo bar diff --git a/app/models/ability.rb b/app/models/ability.rb index f3692a5a067..0b6bcbde5d9 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,35 +1,20 @@ +require_dependency 'declarative_policy' + class Ability class << self # Given a list of users and a project this method returns the users that can # read the given project. def users_that_can_read_project(users, project) - if project.public? - users - else - users.select do |user| - if user.admin? - true - elsif project.internal? && !user.external? - true - elsif project.owner == user - true - elsif project.team.members.include?(user) - true - else - false - end - end + DeclarativePolicy.subject_scope do + users.select { |u| allowed?(u, :read_project, project) } end end # Given a list of users and a snippet this method returns the users that can # read the given snippet. def users_that_can_read_personal_snippet(users, snippet) - case snippet.visibility_level - when Snippet::INTERNAL, Snippet::PUBLIC - users - when Snippet::PRIVATE - users.include?(snippet.author) ? [snippet.author] : [] + DeclarativePolicy.subject_scope do + users.select { |u| allowed?(u, :read_personal_snippet, snippet) } end end @@ -38,42 +23,35 @@ class Ability # issues - The issues to reduce down to those readable by the user. # user - The User for which to check the issues def issues_readable_by_user(issues, user = nil) - return issues if user && user.admin? - - issues.select { |issue| issue.visible_to_user?(user) } + DeclarativePolicy.user_scope do + issues.select { |issue| issue.visible_to_user?(user) } + end end - # TODO: make this private and use the actual abilities stuff for this def can_edit_note?(user, note) - return false if !note.editable? || !user.present? - return true if note.author == user || user.admin? - - if note.project - max_access_level = note.project.team.max_member_access(user.id) - max_access_level >= Gitlab::Access::MASTER - else - false - end + allowed?(user, :edit_note, note) end - def allowed?(user, action, subject = :global) - allowed(user, subject).include?(action) - end + def allowed?(user, action, subject = :global, opts = {}) + if subject.is_a?(Hash) + opts, subject = subject, :global + end - def allowed(user, subject = :global) - return BasePolicy::RuleSet.none if subject.nil? - return uncached_allowed(user, subject) unless RequestStore.active? + policy = policy_for(user, subject) - user_key = user ? user.id : 'anonymous' - subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}" - key = "/ability/#{user_key}/#{subject_key}" - RequestStore[key] ||= uncached_allowed(user, subject).freeze + case opts[:scope] + when :user + DeclarativePolicy.user_scope { policy.can?(action) } + when :subject + DeclarativePolicy.subject_scope { policy.can?(action) } + else + policy.can?(action) + end end - private - - def uncached_allowed(user, subject) - BasePolicy.class_for(subject).abilities(user, subject) + def policy_for(user, subject = :global) + cache = RequestStore.active? ? RequestStore : {} + DeclarativePolicy.policy_for(user, subject, cache: cache) end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 1b3e5a25ac2..364858964b0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -140,6 +140,7 @@ module Ci where(id: max_id) end end + scope :internal, -> { where(source: internal_sources) } def self.latest_status(ref = nil) latest(ref).status @@ -163,6 +164,10 @@ module Ci where.not(duration: nil).sum(:duration) end + def self.internal_sources + sources.reject { |source| source == "external" }.values + end + def stages_count statuses.select(:stage).distinct.count end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index f235260208f..96d6e120998 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,27 +1,12 @@ module Ci class Variable < ActiveRecord::Base extend Ci::Model + include HasVariable belongs_to :project - validates :key, - presence: true, - uniqueness: { scope: :project_id }, - length: { maximum: 255 }, - format: { with: /\A[a-zA-Z0-9_]+\z/, - message: "can contain only letters, digits and '_'." } + validates :key, uniqueness: { scope: :project_id } - scope :order_key_asc, -> { reorder(key: :asc) } scope :unprotected, -> { where(protected: false) } - - attr_encrypted :value, - mode: :per_attribute_iv_and_salt, - insecure_mode: true, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - def to_runner_variable - { key: key, value: value, public: false } - end end end diff --git a/app/models/concerns/feature_gate.rb b/app/models/concerns/feature_gate.rb new file mode 100644 index 00000000000..5db64fe82c4 --- /dev/null +++ b/app/models/concerns/feature_gate.rb @@ -0,0 +1,7 @@ +module FeatureGate + def flipper_id + return nil if new_record? + + "#{self.class.name}:#{id}" + end +end 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/has_variable.rb b/app/models/concerns/has_variable.rb new file mode 100644 index 00000000000..9585b5583dc --- /dev/null +++ b/app/models/concerns/has_variable.rb @@ -0,0 +1,23 @@ +module HasVariable + extend ActiveSupport::Concern + + included do + validates :key, + presence: true, + length: { maximum: 255 }, + format: { with: /\A[a-zA-Z0-9_]+\z/, + message: "can contain only letters, digits and '_'." } + + scope :order_key_asc, -> { reorder(key: :asc) } + + attr_encrypted :value, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' + + def to_runner_variable + { key: key, value: value, public: false } + end + end +end diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb new file mode 100644 index 00000000000..c28974a3cdf --- /dev/null +++ b/app/models/concerns/sha_attribute.rb @@ -0,0 +1,18 @@ +module ShaAttribute + extend ActiveSupport::Concern + + module ClassMethods + def sha_attribute(name) + column = columns.find { |c| c.name == name.to_s } + + # In case the table doesn't exist we won't be able to find the column, + # thus we will only check the type if the column is present. + if column && column.type != :binary + raise ArgumentError, + "sha_attribute #{name.inspect} is invalid since the column type is not :binary" + end + + attribute(name, Gitlab::Database::ShaAttribute.new) + end + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 7ad36f1d80c..66c96d0f586 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -45,6 +45,7 @@ class Environment < ActiveRecord::Base .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 diff --git a/app/models/group.rb b/app/models/group.rb index 0b93460d473..a6fdb30f84c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -222,6 +222,12 @@ class Group < Namespace User.where(id: members_with_parents.select(:user_id)) end + def users_with_descendants + members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) + + User.where(id: members_with_descendants.select(:user_id)) + end + def max_member_access_for_user(user) return GroupMember::OWNER if user.admin? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f581a25f093..c099d731082 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -771,6 +771,7 @@ class MergeRequest < ActiveRecord::Base "refs/heads/#{source_branch}", ref_path ) + update_column(:ref_fetched, true) end def ref_path @@ -778,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 diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 583d4fb5244..672eab94c07 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,5 +1,5 @@ class Namespace < ActiveRecord::Base - acts_as_paranoid + acts_as_paranoid without_default_scope: true include CacheMarkdownField include Sortable @@ -219,6 +219,12 @@ class Namespace < ActiveRecord::Base parent.present? end + def soft_delete_without_removing_associations + # We can't use paranoia's `#destroy` since this will hard-delete projects. + # Project uses `pending_delete` instead of the acts_as_paranoia gem. + self.deleted_at = Time.now + end + private def repository_storage_paths 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/notification_setting.rb b/app/models/notification_setting.rb index b0df7aeb323..81844b1e2ca 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -19,7 +19,7 @@ class NotificationSetting < ActiveRecord::Base # pending delete). # scope :for_projects, -> do - includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil }) + includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil, pending_delete: true }) end EMAIL_EVENTS = [ diff --git a/app/models/project.rb b/app/models/project.rb index 2c2685875f8..a75c5209955 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -222,9 +222,8 @@ class Project < ActiveRecord::Base has_many :uploads, as: :model, dependent: :destroy # Scopes - default_scope { where(pending_delete: false) } - - scope :with_deleted, -> { unscope(where: :pending_delete) } + scope :pending_delete, -> { where(pending_delete: true) } + scope :without_deleted, -> { where(pending_delete: false) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } @@ -350,7 +349,19 @@ 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 + + if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists? + project.run_after_commit do + begin + Projects::HousekeepingService.new(project).execute + rescue Projects::HousekeepingService::LeaseTaken => e + Rails.logger.info("Could not perform housekeeping for project #{project.path_with_namespace} (#{project.id}): #{e}") + end + end + end + end end class << self @@ -507,10 +518,6 @@ class Project < ActiveRecord::Base ProjectCacheWorker.perform_async(self.id) end - remove_import_data - end - - def remove_import_data import_data&.destroy end @@ -689,7 +696,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 @@ -1079,6 +1086,10 @@ class Project < ActiveRecord::Base end end + def ensure_repository + create_repository unless repository_exists? + end + def repository_exists? !!repository.exists? end @@ -1441,7 +1452,7 @@ class Project < ActiveRecord::Base def pending_delete_twin return false unless path - Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) + Project.pending_delete.find_by_full_path(path_with_namespace) end ## diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 48edd0738ee..c8fabb16dc1 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -51,8 +51,11 @@ class ProjectFeature < ActiveRecord::Base default_value_for :repository_access_level, value: ENABLED, allows_nil: false def feature_available?(feature, user) - access_level = public_send(ProjectFeature.access_level_attribute(feature)) - get_permission(user, access_level) + get_permission(user, access_level(feature)) + end + + def access_level(feature) + public_send(ProjectFeature.access_level_attribute(feature)) end def builds_enabled? diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index c9df678f97e..217f753f05f 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -60,7 +60,7 @@ class PrometheusService < MonitoringService def deployment_metrics(deployment) metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics)) - metrics&.merge(deployment_time: created_at.to_i) || {} + metrics&.merge(deployment_time: deployment.created_at.to_i) || {} end def additional_environment_metrics(environment) diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index f38fbda7839..f26ee57510c 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -149,6 +149,10 @@ class ProjectWiki wiki end + def ensure_repository + create_repo! unless repository_exists? + end + def hook_attrs { web_url: web_url, diff --git a/app/models/repository.rb b/app/models/repository.rb index c67475357d9..8c24e722a8b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -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/user.rb b/app/models/user.rb index 9971e43146a..0febae84873 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,6 +11,7 @@ class User < ActiveRecord::Base include CaseSensitivity include TokenAuthenticatable include IgnorableColumn + include FeatureGate DEFAULT_NOTIFICATION_LEVEL = :participating @@ -53,7 +54,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 @@ -299,11 +300,20 @@ class User < ActiveRecord::Base table = arel_table pattern = "%#{query}%" + order = <<~SQL + CASE + WHEN users.name = %{query} THEN 0 + WHEN users.username = %{query} THEN 1 + WHEN users.email = %{query} THEN 2 + ELSE 3 + END + SQL + where( table[:name].matches(pattern) .or(table[:email].matches(pattern)) .or(table[:username].matches(pattern)) - ) + ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, id: :desc) end # searches user by given pattern @@ -494,10 +504,8 @@ 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 @@ -572,7 +580,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? @@ -965,7 +979,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 @@ -1129,7 +1143,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/policies/base_policy.rb b/app/policies/base_policy.rb index 261a2e780c5..a605a3457c8 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -1,133 +1,20 @@ -class BasePolicy - include Gitlab::CurrentSettings - - class RuleSet - attr_reader :can_set, :cannot_set - def initialize(can_set, cannot_set) - @can_set = can_set - @cannot_set = cannot_set - end - - delegate :size, to: :to_set - - def self.empty - new(Set.new, Set.new) - end - - def self.none - empty.freeze - end - - def can?(ability) - @can_set.include?(ability) && !@cannot_set.include?(ability) - end - - def include?(ability) - can?(ability) - end - - def to_set - @can_set - @cannot_set - end - - def merge(other) - @can_set.merge(other.can_set) - @cannot_set.merge(other.cannot_set) - end - - def can!(*abilities) - @can_set.merge(abilities) - end - - def cannot!(*abilities) - @cannot_set.merge(abilities) - end - - def freeze - @can_set.freeze - @cannot_set.freeze - super - end - end - - def self.abilities(user, subject) - new(user, subject).abilities - end - - def self.class_for(subject) - return GlobalPolicy if subject == :global - raise ArgumentError, 'no policy for nil' if subject.nil? - - if subject.class.try(:presenter?) - subject = subject.subject - end - - subject.class.ancestors.each do |klass| - next unless klass.name - - begin - policy_class = "#{klass.name}Policy".constantize - - # NOTE: the < operator here tests whether policy_class - # inherits from BasePolicy - return policy_class if policy_class < BasePolicy - rescue NameError - nil - end - end +require_dependency 'declarative_policy' - raise "no policy for #{subject.class.name}" - end - - attr_reader :user, :subject - def initialize(user, subject) - @user = user - @subject = subject - end - - def abilities - return RuleSet.none if @user && @user.blocked? - return anonymous_abilities if @user.nil? - collect_rules { rules } - end - - def anonymous_abilities - collect_rules { anonymous_rules } - end - - def anonymous_rules - rules - end - - def rules - raise NotImplementedError - end - - def delegate!(new_subject) - @rule_set.merge(Ability.allowed(@user, new_subject)) - end - - def can?(rule) - @rule_set.can?(rule) - end +class BasePolicy < DeclarativePolicy::Base + include Gitlab::CurrentSettings - def can!(*rules) - @rule_set.can!(*rules) - end + desc "User is an instance admin" + with_options scope: :user, score: 0 + condition(:admin) { @user&.admin? } - def cannot!(*rules) - @rule_set.cannot!(*rules) - end + with_options scope: :user, score: 0 + condition(:external_user) { @user.nil? || @user.external? } - private - - def collect_rules(&b) - @rule_set = RuleSet.empty - yield - @rule_set - end + with_options scope: :user, score: 0 + condition(:can_create_group) { @user&.can_create_group } - def restricted_public_level? + desc "The application is restricted from public visibility" + condition(:restricted_public_level, scope: :global) do current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) end end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 2d7405dc240..a886efc1360 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,29 +1,13 @@ module Ci class BuildPolicy < CommitStatusPolicy - alias_method :build, :subject - - def rules - super - - # If we can't read build we should also not have that - # ability when looking at this in context of commit_status - %w[read create update admin].each do |rule| - cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" - end - - if can?(:update_build) && protected_action? - cannot! :update_build - end - end - - private - - def protected_action? - return false unless build.action? + condition(:protected_action) do + next false unless @subject.action? !::Gitlab::UserAccess - .new(user, project: build.project) - .can_merge_to_branch?(build.ref) + .new(@user, project: @subject.project) + .can_merge_to_branch?(@subject.ref) end + + rule { protected_action }.prevent :update_build end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 10aa2d3e72a..a2dde95dbc8 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -1,7 +1,5 @@ module Ci class PipelinePolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end end diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb index 416d93ffe63..7dff8470e23 100644 --- a/app/policies/ci/runner_policy.rb +++ b/app/policies/ci/runner_policy.rb @@ -1,13 +1,16 @@ module Ci class RunnerPolicy < BasePolicy - def rules - return unless @user + with_options scope: :subject, score: 0 + condition(:shared) { @subject.is_shared? } - can! :assign_runner if @user.admin? + with_options scope: :subject, score: 0 + condition(:locked, scope: :subject) { @subject.locked? } - return if @subject.is_shared? || @subject.locked? + condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) } - can! :assign_runner if @user.ci_authorized_runners.include?(@subject) - end + rule { anonymous }.prevent_all + rule { admin | authorized_runner }.enable :assign_runner + rule { ~admin & shared }.prevent :assign_runner + rule { ~admin & locked }.prevent :assign_runner end end diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb index c90c9ac0583..5592ac30812 100644 --- a/app/policies/ci/trigger_policy.rb +++ b/app/policies/ci/trigger_policy.rb @@ -1,13 +1,16 @@ module Ci class TriggerPolicy < BasePolicy - def rules - delegate! @subject.project - - if can?(:admin_build) - can! :admin_trigger if @subject.owner.blank? || - @subject.owner == @user - can! :manage_trigger - end - end + delegate { @subject.project } + + with_options scope: :subject, score: 0 + condition(:legacy) { @subject.legacy? } + + with_score 0 + condition(:is_owner) { @user && @subject.owner_id == @user.id } + + rule { ~can?(:admin_build) }.prevent :admin_trigger + rule { legacy | is_owner }.enable :admin_trigger + + rule { can?(:admin_build) }.enable :manage_trigger end end diff --git a/app/policies/commit_status_policy.rb b/app/policies/commit_status_policy.rb index 593df738328..24b2a4cc7fd 100644 --- a/app/policies/commit_status_policy.rb +++ b/app/policies/commit_status_policy.rb @@ -1,5 +1,7 @@ class CommitStatusPolicy < BasePolicy - def rules - delegate! @subject.project + delegate { @subject.project } + + %w[read create update admin].each do |action| + rule { ~can?(:"#{action}_commit_status") }.prevent :"#{action}_build" end end diff --git a/app/policies/deploy_key_policy.rb b/app/policies/deploy_key_policy.rb index ebab213e6be..62a22a59be6 100644 --- a/app/policies/deploy_key_policy.rb +++ b/app/policies/deploy_key_policy.rb @@ -1,11 +1,11 @@ class DeployKeyPolicy < BasePolicy - def rules - return unless @user + with_options scope: :subject, score: 0 + condition(:private_deploy_key) { @subject.private? } - can! :update_deploy_key if @user.admin? + condition(:has_deploy_key) { @user.project_deploy_keys.exists?(id: @subject.id) } - if @subject.private? && @user.project_deploy_keys.exists?(id: @subject.id) - can! :update_deploy_key - end - end + rule { anonymous }.prevent_all + + rule { admin }.enable :update_deploy_key + rule { private_deploy_key & has_deploy_key }.enable :update_deploy_key end diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb index 163d070ff90..62b63b9f87b 100644 --- a/app/policies/deployment_policy.rb +++ b/app/policies/deployment_policy.rb @@ -1,5 +1,3 @@ class DeploymentPolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb index 2fa15e64562..375a5535359 100644 --- a/app/policies/environment_policy.rb +++ b/app/policies/environment_policy.rb @@ -1,17 +1,9 @@ class EnvironmentPolicy < BasePolicy - alias_method :environment, :subject + delegate { @subject.project } - def rules - delegate! environment.project - - if can?(:create_deployment) && environment.stop_action? - can! :stop_environment if can_play_stop_action? - end + condition(:stop_action_allowed) do + @subject.stop_action? && can?(:update_build, @subject.stop_action) end - private - - def can_play_stop_action? - Ability.allowed?(user, :update_build, environment.stop_action) - end + rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment end diff --git a/app/policies/external_issue_policy.rb b/app/policies/external_issue_policy.rb index d9e28bd107a..e031b38078c 100644 --- a/app/policies/external_issue_policy.rb +++ b/app/policies/external_issue_policy.rb @@ -1,5 +1,3 @@ class ExternalIssuePolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index e9be43a5037..7767d3cccd5 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -1,17 +1,51 @@ class GlobalPolicy < BasePolicy - def rules - can! :read_users_list unless restricted_public_level? + desc "User is blocked" + with_options scope: :user, score: 0 + condition(:blocked) { @user.blocked? } - return unless @user + desc "User is an internal user" + with_options scope: :user, score: 0 + condition(:internal) { @user.internal? } - can! :create_group if @user.can_create_group + desc "User's access has been locked" + with_options scope: :user, score: 0 + condition(:access_locked) { @user.access_locked? } - unless @user.blocked? || @user.internal? - can! :log_in unless @user.access_locked? - can! :access_api - can! :access_git - can! :receive_notifications - can! :use_quick_actions - end + rule { anonymous }.policy do + prevent :log_in + prevent :access_api + prevent :access_git + prevent :receive_notifications + prevent :use_quick_actions + prevent :create_group + prevent :log_in + end + + rule { default }.policy do + enable :log_in + enable :access_api + enable :access_git + enable :receive_notifications + enable :use_quick_actions + end + + rule { blocked | internal }.policy do + prevent :log_in + prevent :access_api + prevent :access_git + prevent :receive_notifications + prevent :use_quick_actions + end + + rule { can_create_group }.policy do + enable :create_group + end + + rule { access_locked }.policy do + prevent :log_in + end + + rule { ~restricted_public_level }.policy do + enable :read_users_list end end diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb index 7b34aa182eb..e3dd3296699 100644 --- a/app/policies/group_label_policy.rb +++ b/app/policies/group_label_policy.rb @@ -1,5 +1,3 @@ class GroupLabelPolicy < BasePolicy - def rules - delegate! @subject.group - end + delegate { @subject.group } end diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index 5a3fe814b77..23dd0d7cd23 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -1,25 +1,22 @@ class GroupMemberPolicy < BasePolicy - def rules - return unless @user + delegate :group - target_user = @subject.user - group = @subject.group + with_scope :subject + condition(:last_owner) { @subject.group.last_owner?(@subject.user) } - return if group.last_owner?(target_user) + desc "Membership is users' own" + with_score 0 + condition(:is_target_user) { @user && @subject.user_id == @user.id } - can_manage = Ability.allowed?(@user, :admin_group_member, group) + rule { anonymous }.prevent_all + rule { last_owner }.prevent_all - if can_manage - can! :update_group_member - can! :destroy_group_member - elsif @user == target_user - can! :destroy_group_member - end - - additional_rules! + rule { can?(:admin_group_member) }.policy do + enable :update_group_member + enable :destroy_group_member end - def additional_rules! - # This is meant to be overriden in EE + rule { is_target_user }.policy do + enable :destroy_group_member end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index fb07298c6c2..dcb37416ca3 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -1,50 +1,58 @@ class GroupPolicy < BasePolicy - def rules - can! :read_group if @subject.public? - return unless @user - - globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) - access_level = @subject.max_member_access_for_user(@user) - owner = access_level >= GroupMember::OWNER - master = access_level >= GroupMember::MASTER - reporter = access_level >= GroupMember::REPORTER - - can_read = false - can_read ||= globally_viewable - can_read ||= access_level >= GroupMember::GUEST - can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? - can! :read_group if can_read - - if reporter - can! :admin_label - end - - # Only group masters and group owners can create new projects - if master - can! :create_projects - can! :admin_milestones - end - - # Only group owner and administrators can admin group - if owner - can! :admin_group - can! :admin_namespace - can! :admin_group_member - can! :change_visibility_level - can! :create_subgroup if @user.can_create_group - end - - if globally_viewable && @subject.request_access_enabled && access_level == GroupMember::NO_ACCESS - can! :request_access - end - end + desc "Group is public" + with_options scope: :subject, score: 0 + condition(:public_group) { @subject.public? } + + with_score 0 + condition(:logged_in_viewable) { @user && @subject.internal? && !@user.external? } + + condition(:has_access) { access_level != GroupMember::NO_ACCESS } - def can_read_group? - return true if @subject.public? - return true if @user.admin? - return true if @subject.internal? && !@user.external? - return true if @subject.users.include?(@user) + condition(:guest) { access_level >= GroupMember::GUEST } + condition(:owner) { access_level >= GroupMember::OWNER } + condition(:master) { access_level >= GroupMember::MASTER } + condition(:reporter) { access_level >= GroupMember::REPORTER } + condition(:has_projects) do GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? end + + with_options scope: :subject, score: 0 + condition(:request_access_enabled) { @subject.request_access_enabled } + + rule { public_group } .enable :read_group + rule { logged_in_viewable }.enable :read_group + rule { guest } .enable :read_group + rule { admin } .enable :read_group + rule { has_projects } .enable :read_group + + rule { reporter }.enable :admin_label + + rule { master }.policy do + enable :create_projects + enable :admin_milestones + end + + rule { owner }.policy do + enable :admin_group + enable :admin_namespace + enable :admin_group_member + enable :change_visibility_level + end + + rule { owner & can_create_group }.enable :create_subgroup + + rule { public_group | logged_in_viewable }.enable :view_globally + + rule { default }.enable(:request_access) + + rule { ~request_access_enabled }.prevent :request_access + rule { ~can?(:view_globally) }.prevent :request_access + rule { has_access }.prevent :request_access + + def access_level + return GroupMember::NO_ACCESS if @user.nil? + + @access_level ||= @subject.max_member_access_for_user(@user) + end end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index 9501e499507..daf6fa9e18a 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -1,14 +1,15 @@ class IssuablePolicy < BasePolicy - def action_name - @subject.class.name.underscore - end + delegate { @subject.project } - def rules - if @user && @subject.assignee_or_author?(@user) - can! :"read_#{action_name}" - can! :"update_#{action_name}" - end + desc "User is the assignee or author" + condition(:assignee_or_author) do + @user && @subject.assignee_or_author?(@user) + end - delegate! @subject.project + rule { assignee_or_author }.policy do + enable :read_issue + enable :update_issue + enable :read_merge_request + enable :update_merge_request end end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index 88f3179c6ff..bd2d417b2a8 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -3,25 +3,17 @@ class IssuePolicy < IssuablePolicy # Make sure to sync this class checks with issue.rb to avoid security problems. # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. - def issue - @subject + desc "User can read confidential issues" + condition(:can_read_confidential) do + @user && IssueCollection.new([@subject]).visible_to(@user).any? end - def rules - super + desc "Issue is confidential" + condition(:confidential, scope: :subject) { @subject.confidential? } - if @subject.confidential? && !can_read_confidential? - cannot! :read_issue - cannot! :update_issue - cannot! :admin_issue - end - end - - private - - def can_read_confidential? - return false unless @user - - IssueCollection.new([@subject]).visible_to(@user).any? + rule { confidential & ~can_read_confidential }.policy do + prevent :read_issue + prevent :update_issue + prevent :admin_issue end end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb index 29bb357e00a..85b67f0a237 100644 --- a/app/policies/namespace_policy.rb +++ b/app/policies/namespace_policy.rb @@ -1,10 +1,10 @@ class NamespacePolicy < BasePolicy - def rules - return unless @user + rule { anonymous }.prevent_all - if @subject.owner == @user || @user.admin? - can! :create_projects - can! :admin_namespace - end + condition(:owner) { @subject.owner == @user } + + rule { owner | admin }.policy do + enable :create_projects + enable :admin_namespace end end diff --git a/app/policies/nil_policy.rb b/app/policies/nil_policy.rb new file mode 100644 index 00000000000..13f46ba60f0 --- /dev/null +++ b/app/policies/nil_policy.rb @@ -0,0 +1,3 @@ +class NilPolicy < BasePolicy + rule { default }.prevent_all +end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 5326061bd07..20cd51cfb99 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -1,19 +1,24 @@ class NotePolicy < BasePolicy - def rules - delegate! @subject.project + delegate { @subject.project } - return unless @user + condition(:is_author) { @user && @subject.author == @user } + condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? } + condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id } - if @subject.author == @user - can! :read_note - can! :update_note - can! :admin_note - can! :resolve_note - end + condition(:editable, scope: :subject) { @subject.editable? } - if @subject.for_merge_request? && - @subject.noteable.author == @user - can! :resolve_note - end + rule { ~editable | anonymous }.prevent :edit_note + rule { is_author | admin }.enable :edit_note + rule { can?(:master_access) }.enable :edit_note + + rule { is_author }.policy do + enable :read_note + enable :update_note + enable :admin_note + enable :resolve_note + end + + rule { for_merge_request & is_noteable_author }.policy do + enable :resolve_note end end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index e1e5336da8c..cac0530b9f7 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -1,27 +1,28 @@ class PersonalSnippetPolicy < BasePolicy - def rules - can! :read_personal_snippet if @subject.public? - return unless @user + condition(:public_snippet, scope: :subject) { @subject.public? } + condition(:is_author) { @user && @subject.author == @user } + condition(:internal_snippet, scope: :subject) { @subject.internal? } - if @subject.public? - can! :comment_personal_snippet - end + rule { public_snippet }.policy do + enable :read_personal_snippet + enable :comment_personal_snippet + end - if @subject.author == @user - can! :read_personal_snippet - can! :update_personal_snippet - can! :destroy_personal_snippet - can! :admin_personal_snippet - can! :comment_personal_snippet - end + rule { is_author }.policy do + enable :read_personal_snippet + enable :update_personal_snippet + enable :destroy_personal_snippet + enable :admin_personal_snippet + enable :comment_personal_snippet + end - unless @user.external? - can! :create_personal_snippet - end + rule { ~anonymous }.enable :create_personal_snippet + rule { external_user }.prevent :create_personal_snippet - if @subject.internal? && !@user.external? - can! :read_personal_snippet - can! :comment_personal_snippet - end + rule { internal_snippet & ~external_user }.policy do + enable :read_personal_snippet + enable :comment_personal_snippet end + + rule { anonymous }.prevent :comment_personal_snippet end diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb index b12b4c5166b..2d0f021118b 100644 --- a/app/policies/project_label_policy.rb +++ b/app/policies/project_label_policy.rb @@ -1,5 +1,3 @@ class ProjectLabelPolicy < BasePolicy - def rules - delegate! @subject.project - end + delegate { @subject.project } end diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb index 1c038dddd4b..9aedb620be9 100644 --- a/app/policies/project_member_policy.rb +++ b/app/policies/project_member_policy.rb @@ -1,22 +1,16 @@ class ProjectMemberPolicy < BasePolicy - def rules - # anonymous users have no abilities here - return unless @user + delegate { @subject.project } - target_user = @subject.user - project = @subject.project + condition(:target_is_owner, scope: :subject) { @subject.user == @subject.project.owner } + condition(:target_is_self) { @user && @subject.user == @user } - return if target_user == project.owner + rule { anonymous }.prevent_all + rule { target_is_owner }.prevent_all - can_manage = Ability.allowed?(@user, :admin_project_member, project) - - if can_manage - can! :update_project_member - can! :destroy_project_member - end - - if @user == target_user - can! :destroy_project_member - end + rule { can?(:admin_project_member) }.policy do + enable :update_project_member + enable :destroy_project_member end + + rule { target_is_self }.enable :destroy_project_member end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 47518dddb61..7cbca63fab4 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -1,297 +1,353 @@ class ProjectPolicy < BasePolicy - def rules - team_access!(user) + def self.create_read_update_admin(name) + [ + :"create_#{name}", + :"read_#{name}", + :"update_#{name}", + :"admin_#{name}" + ] + end - owner_access! if user.admin? || owner? - team_member_owner_access! if owner? + desc "User is a project owner" + condition :owner do + @user && project.owner == @user || (project.group && project.group.has_owner?(@user)) + end - if project.public? || (project.internal? && !user.external?) - guest_access! - public_access! - can! :request_access if access_requestable? - end + desc "Project has public builds enabled" + condition(:public_builds, scope: :subject) { project.public_builds? } + + # For guest access we use #is_team_member? so we can use + # project.members, which gets cached in subject scope. + # This is safe because team_access_level is guaranteed + # by ProjectAuthorization's validation to be at minimum + # GUEST + desc "User has guest access" + condition(:guest) { is_team_member? } - archived_access! if project.archived? + desc "User has reporter access" + condition(:reporter) { team_access_level >= Gitlab::Access::REPORTER } - disabled_features! + desc "User has developer access" + condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER } + + desc "User has master access" + condition(:master) { team_access_level >= Gitlab::Access::MASTER } + + desc "Project is public" + condition(:public_project, scope: :subject) { project.public? } + + desc "Project is visible to internal users" + condition(:internal_access) do + project.internal? && !user.external? end - def project - @subject + desc "User is a member of the group" + condition(:group_member, scope: :subject) { project_group_member? } + + desc "Project is archived" + condition(:archived, scope: :subject) { project.archived? } + + condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? } + + desc "Container registry is disabled" + condition(:container_registry_disabled, scope: :subject) do + !project.container_registry_enabled end - def owner? - return @owner if defined?(@owner) - - @owner = project.owner == user || - (project.group && project.group.has_owner?(user)) - end - - def guest_access! - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_issue - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_note - can! :create_project - can! :create_issue - can! :create_note - can! :upload_file - can! :read_cycle_analytics - - if project.public_builds? - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_build - end + desc "Project has an external wiki" + condition(:has_external_wiki, scope: :subject) { project.has_external_wiki? } + + desc "Project has request access enabled" + condition(:request_access_enabled, scope: :subject) { project.request_access_enabled } + + features = %w[ + merge_requests + issues + repository + snippets + wiki + builds + ] + + features.each do |f| + # these are scored high because they are unlikely + desc "Project has #{f} disabled" + condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) } end - def reporter_access! - can! :download_code - can! :download_wiki_code - can! :fork_project - can! :create_project_snippet - can! :update_issue - can! :admin_issue - can! :admin_label - can! :admin_list - can! :read_commit_status - can! :read_build - can! :read_container_image - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_environment - can! :read_deployment - can! :read_merge_request - end - - # Permissions given when an user is team member of a project - def team_member_reporter_access! - can! :build_download_code - can! :build_read_container_image - end - - def developer_access! - can! :admin_merge_request - can! :update_merge_request - can! :create_commit_status - can! :update_commit_status - can! :create_build - can! :update_build - can! :create_pipeline - can! :update_pipeline - can! :create_pipeline_schedule - can! :update_pipeline_schedule - can! :create_merge_request - can! :create_wiki - can! :push_code - can! :resolve_note - can! :create_container_image - can! :update_container_image - can! :create_environment - can! :create_deployment - end - - def master_access! - can! :delete_protected_branch - can! :update_project_snippet - can! :update_environment - can! :update_deployment - can! :admin_milestone - can! :admin_project_snippet - can! :admin_project_member - can! :admin_note - can! :admin_wiki - can! :admin_project - can! :admin_commit_status - can! :admin_build - can! :admin_container_image - can! :admin_pipeline - can! :admin_pipeline_schedule - can! :admin_environment - can! :admin_deployment - can! :admin_pages - can! :read_pages - can! :update_pages - end - - def public_access! - can! :download_code - can! :fork_project - can! :read_commit_status - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_container_image - can! :build_download_code - can! :build_read_container_image - can! :read_merge_request - end - - def owner_access! - guest_access! - reporter_access! - developer_access! - master_access! - can! :change_namespace - can! :change_visibility_level - can! :rename_project - can! :remove_project - can! :archive_project - can! :remove_fork_project - can! :destroy_merge_request - can! :destroy_issue - can! :remove_pages - end - - def team_member_owner_access! - team_member_reporter_access! - end - - # Push abilities on the users team role - def team_access!(user) - access = project.team.max_member_access(user.id) - - return if access < Gitlab::Access::GUEST - guest_access! - - return if access < Gitlab::Access::REPORTER - reporter_access! - team_member_reporter_access! - - return if access < Gitlab::Access::DEVELOPER - developer_access! - - return if access < Gitlab::Access::MASTER - master_access! - end - - def archived_access! - cannot! :create_merge_request - cannot! :push_code - cannot! :delete_protected_branch - cannot! :update_merge_request - cannot! :admin_merge_request - end - - def disabled_features! - repository_enabled = project.feature_available?(:repository, user) - - block_issues_abilities - - unless project.feature_available?(:merge_requests, user) && repository_enabled - cannot!(*named_abilities(:merge_request)) - end + rule { guest }.enable :guest_access + rule { reporter }.enable :reporter_access + rule { developer }.enable :developer_access + rule { master }.enable :master_access + + rule { owner | admin }.policy do + enable :guest_access + enable :reporter_access + enable :developer_access + enable :master_access + + enable :change_namespace + enable :change_visibility_level + enable :rename_project + enable :remove_project + enable :archive_project + enable :remove_fork_project + enable :destroy_merge_request + enable :destroy_issue + enable :remove_pages + end - unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user) - cannot!(*named_abilities(:label)) - cannot!(*named_abilities(:milestone)) - end + rule { owner | reporter }.policy do + enable :build_download_code + enable :build_read_container_image + end - unless project.feature_available?(:snippets, user) - cannot!(*named_abilities(:project_snippet)) - end + rule { can?(:guest_access) }.policy do + enable :read_project + enable :read_board + enable :read_list + enable :read_wiki + enable :read_issue + enable :read_label + enable :read_milestone + enable :read_project_snippet + enable :read_project_member + enable :read_note + enable :create_project + enable :create_issue + enable :create_note + enable :upload_file + enable :read_cycle_analytics + enable :read_project_snippet + end - unless project.feature_available?(:wiki, user) || project.has_external_wiki? - cannot!(*named_abilities(:wiki)) - cannot!(:download_wiki_code) - end + rule { can?(:reporter_access) }.policy do + enable :download_code + enable :download_wiki_code + enable :fork_project + enable :create_project_snippet + enable :update_issue + enable :admin_issue + enable :admin_label + enable :admin_list + enable :read_commit_status + enable :read_build + enable :read_container_image + enable :read_pipeline + enable :read_pipeline_schedule + enable :read_environment + enable :read_deployment + enable :read_merge_request + end - unless project.feature_available?(:builds, user) && repository_enabled - cannot!(*named_abilities(:build)) - cannot!(*named_abilities(:pipeline) - [:read_pipeline]) - cannot!(*named_abilities(:pipeline_schedule)) - cannot!(*named_abilities(:environment)) - cannot!(*named_abilities(:deployment)) - end + rule { (~anonymous & public_project) | internal_access }.policy do + enable :public_user_access + end - unless repository_enabled - cannot! :push_code - cannot! :delete_protected_branch - cannot! :download_code - cannot! :fork_project - cannot! :read_commit_status - end + rule { can?(:public_user_access) }.policy do + enable :guest_access + enable :request_access + end - unless project.container_registry_enabled - cannot!(*named_abilities(:container_image)) - end + rule { owner | admin | guest | group_member }.prevent :request_access + rule { ~request_access_enabled }.prevent :request_access + + rule { can?(:developer_access) }.policy do + enable :admin_merge_request + enable :update_merge_request + enable :create_commit_status + enable :update_commit_status + enable :create_build + enable :update_build + enable :create_pipeline + enable :update_pipeline + enable :create_pipeline_schedule + enable :update_pipeline_schedule + enable :create_merge_request + enable :create_wiki + enable :push_code + enable :resolve_note + enable :create_container_image + enable :update_container_image + enable :create_environment + enable :create_deployment end - def anonymous_rules - return unless project.public? + rule { can?(:master_access) }.policy do + enable :delete_protected_branch + enable :update_project_snippet + enable :update_environment + enable :update_deployment + enable :admin_milestone + enable :admin_project_snippet + enable :admin_project_member + enable :admin_note + enable :admin_wiki + enable :admin_project + enable :admin_commit_status + enable :admin_build + enable :admin_container_image + enable :admin_pipeline + enable :admin_pipeline_schedule + enable :admin_environment + enable :admin_deployment + enable :admin_pages + enable :read_pages + enable :update_pages + end - base_readonly_access! + rule { can?(:public_user_access) }.policy do + enable :public_access - # Allow to read builds by anonymous user if guests are allowed - can! :read_build if project.public_builds? + enable :fork_project + enable :build_download_code + enable :build_read_container_image + end - disabled_features! + rule { archived }.policy do + prevent :create_merge_request + prevent :push_code + prevent :delete_protected_branch + prevent :update_merge_request + prevent :admin_merge_request end - def block_issues_abilities - unless project.feature_available?(:issues, user) - cannot! :read_issue if project.default_issues_tracker? - cannot! :create_issue - cannot! :update_issue - cannot! :admin_issue - end + rule { merge_requests_disabled | repository_disabled }.policy do + prevent(*create_read_update_admin(:merge_request)) end - def named_abilities(name) - [ - :"read_#{name}", - :"create_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] + rule { issues_disabled & merge_requests_disabled }.policy do + prevent(*create_read_update_admin(:label)) + prevent(*create_read_update_admin(:milestone)) + end + + rule { snippets_disabled }.policy do + prevent(*create_read_update_admin(:project_snippet)) + end + + rule { wiki_disabled & ~has_external_wiki }.policy do + prevent(*create_read_update_admin(:wiki)) + prevent(:download_wiki_code) + end + + rule { builds_disabled | repository_disabled }.policy do + prevent(*create_read_update_admin(:build)) + prevent(*(create_read_update_admin(:pipeline) - [:read_pipeline])) + prevent(*create_read_update_admin(:pipeline_schedule)) + prevent(*create_read_update_admin(:environment)) + prevent(*create_read_update_admin(:deployment)) + end + + rule { repository_disabled }.policy do + prevent :push_code + prevent :push_code_to_protected_branches + prevent :download_code + prevent :fork_project + prevent :read_commit_status + end + + rule { container_registry_disabled }.policy do + prevent(*create_read_update_admin(:container_image)) + end + + rule { anonymous & ~public_project }.prevent_all + rule { public_project }.enable(:public_access) + + rule { can?(:public_access) }.policy do + enable :read_project + enable :read_board + enable :read_list + enable :read_wiki + enable :read_label + enable :read_milestone + enable :read_project_snippet + enable :read_project_member + enable :read_merge_request + enable :read_note + enable :read_pipeline + enable :read_pipeline_schedule + enable :read_commit_status + enable :read_container_image + enable :download_code + enable :download_wiki_code + enable :read_cycle_analytics + + # NOTE: may be overridden by IssuePolicy + enable :read_issue + end + + rule { public_builds }.policy do + enable :read_build + end + + rule { public_builds & can?(:guest_access) }.policy do + enable :read_pipeline + enable :read_pipeline_schedule + end + + rule { issues_disabled }.policy do + prevent :create_issue + prevent :update_issue + prevent :admin_issue + end + + rule { issues_disabled & default_issues_tracker }.policy do + prevent :read_issue end private - def project_group_member?(user) + def is_team_member? + return false if @user.nil? + + greedy_load_subject = false + + # when scoping by subject, we want to be greedy + # and load *all* the members with one query. + greedy_load_subject ||= DeclarativePolicy.preferred_scope == :subject + + # in this case we're likely to have loaded #members already + # anyways, and #member? would fail with an error + greedy_load_subject ||= !@user.persisted? + + if greedy_load_subject + project.team.members.include?(user) + else + # otherwise we just make a specific query for + # this particular user. + team_access_level >= Gitlab::Access::GUEST + end + end + + def project_group_member? + return false if @user.nil? + project.group && ( - project.group.members_with_parents.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) + project.group.members_with_parents.exists?(user_id: @user.id) || + project.group.requesters.exists?(user_id: @user.id) ) end - def access_requestable? - project.request_access_enabled && - !owner? && - !user.admin? && - !project.team.member?(user) && - !project_group_member?(user) - end - - # A base set of abilities for read-only users, which - # is then augmented as necessary for anonymous and other - # read-only users. - def base_readonly_access! - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_merge_request - can! :read_note - can! :read_pipeline - can! :read_pipeline_schedule - can! :read_commit_status - can! :read_container_image - can! :download_code - can! :download_wiki_code - can! :read_cycle_analytics + def team_access_level + return -1 if @user.nil? - # NOTE: may be overridden by IssuePolicy - can! :read_issue + # NOTE: max_member_access has its own cache + project.team.max_member_access(@user.id) + end + + def feature_available?(feature) + case project.project_feature.access_level(feature) + when ProjectFeature::DISABLED + false + when ProjectFeature::PRIVATE + guest? || admin? + else + true + end + end + + def project + @subject end end diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index bc5c4f32f79..dd270643bbf 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -1,25 +1,45 @@ class ProjectSnippetPolicy < BasePolicy - def rules - # We have to check both project feature visibility and a snippet visibility and take the stricter one - # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573 - return unless @subject.project.feature_available?(:snippets, @user) - return unless Ability.allowed?(@user, :read_project, @subject.project) - - can! :read_project_snippet if @subject.public? - return unless @user - - if @user && (@subject.author == @user || @user.admin?) - can! :read_project_snippet - can! :update_project_snippet - can! :admin_project_snippet - end - - if @subject.internal? && !@user.external? - can! :read_project_snippet - end - - if @subject.project.team.member?(@user) - can! :read_project_snippet - end + delegate :project + + desc "Snippet is public" + condition(:public_snippet, scope: :subject) { @subject.public? } + condition(:private_snippet, scope: :subject) { @subject.private? } + condition(:public_project, scope: :subject) { @subject.project.public? } + + condition(:is_author) { @user && @subject.author == @user } + + condition(:internal, scope: :subject) { @subject.internal? } + + # We have to check both project feature visibility and a snippet visibility and take the stricter one + # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573 + rule { ~can?(:read_project) }.policy do + prevent :read_project_snippet + prevent :update_project_snippet + prevent :admin_project_snippet + end + + # we have to use this complicated prevent because the delegated project policy + # is overly greedy in allowing :read_project_snippet, since it doesn't have any + # information about the snippet. However, :read_project_snippet on the *project* + # is used to hide/show various snippet-related controls, so we can't just move + # all of the handling here. + rule do + all?(private_snippet | (internal & external_user), + ~project.guest, + ~admin, + ~is_author) + end.prevent :read_project_snippet + + rule { internal & ~is_author & ~admin }.policy do + prevent :update_project_snippet + prevent :admin_project_snippet + end + + rule { public_snippet }.enable :read_project_snippet + + rule { is_author | admin }.policy do + enable :read_project_snippet + enable :update_project_snippet + enable :admin_project_snippet end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 265c56aba53..0905ddd9b38 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,13 +1,13 @@ class UserPolicy < BasePolicy - def rules - can! :read_user if @user || !restricted_public_level? + desc "The current user is the user in question" + condition(:user_is_self, score: 0) { @subject == @user } - if @user - if @user.admin? || @subject == @user - can! :destroy_user - end + desc "This is the ghost user" + condition(:subject_ghost, scope: :subject, score: 0) { @subject.ghost? } - cannot! :destroy_user if @subject.ghost? - end - end + rule { ~restricted_public_level }.enable :read_user + rule { ~anonymous }.enable :read_user + + rule { user_is_self | admin }.enable :destroy_user + rule { subject_ghost }.prevent :destroy_user end 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/register_job_service.rb b/app/services/ci/register_job_service.rb index af84d4c7427..b951e8d0c9f 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -54,7 +54,7 @@ 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(:project).where(projects: { shared_runners_enabled: true, pending_delete: false }) .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'). @@ -66,7 +66,7 @@ module Ci end def builds_for_specific_runner - new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC') + new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC') end def running_builds_for_shared_runners 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/git_hooks_service.rb b/app/services/git_hooks_service.rb index d222d1e63aa..eab65d09299 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -3,8 +3,8 @@ class GitHooksService attr_accessor :oldrev, :newrev, :ref - def execute(user, repo_path, oldrev, newrev, ref) - @repo_path = repo_path + def execute(user, project, oldrev, newrev, ref) + @project = project @user = Gitlab::GlId.gl_id(user) @oldrev = oldrev @newrev = newrev @@ -26,7 +26,7 @@ class GitHooksService private def run_hook(name) - hook = Gitlab::Git::Hook.new(name, @repo_path) + hook = Gitlab::Git::Hook.new(name, @project) hook.trigger(@user, oldrev, newrev, ref) end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index ed6ea638235..43636fde0be 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -120,7 +120,7 @@ class GitOperationService def with_hooks(ref, newrev, oldrev) GitHooksService.new.execute( user, - repository.path_to_repo, + repository.project, oldrev, newrev, ref) do |service| diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 497fdb09cdc..80c51cb5a72 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -1,8 +1,7 @@ module Groups class DestroyService < Groups::BaseService def async_execute - # Soft delete via paranoia gem - group.destroy + group.soft_delete_without_removing_associations job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end @@ -10,7 +9,7 @@ module Groups def execute group.prepare_for_destroy - group.projects.with_deleted.each do |project| + group.projects.each do |project| # Execute the destruction of the models immediately to ensure atomic cleanup. # Skip repository removal because we remove directory with namespace # that contain all these repositories diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index f00a33969a8..5dd40e07c0d 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -49,7 +49,7 @@ module MergeRequests def url_for_new_merge_request(branch_name) merge_request_params = { source_branch: branch_name } - url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params) + url = Gitlab::Routing.url_helpers.namespace_project_new_merge_request_url(project.namespace, project, merge_request: merge_request_params) { branch_name: branch_name, url: url, new_merge_request: true } end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 8d1820bc504..9ac561e4bd2 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -11,7 +11,7 @@ class NotificationRecipientService 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) + recipients = participants(target, current_user) recipients = add_project_watchers(recipients) recipients = add_custom_notifications(recipients, custom_action) recipients = reject_mention_users(recipients) @@ -86,12 +86,7 @@ class NotificationRecipientService mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } # Add all users participating in the thread (author, assignee, comment authors) - recipients = - if target.respond_to?(:participants) - target.participants(note.author) - else - mentioned_users - end + recipients = participants(target, note.author) || mentioned_users unless note.for_personal_snippet? # Merge project watchers @@ -123,6 +118,14 @@ class NotificationRecipientService protected + # Ensure that if we modify this array, we aren't modifying the memoised + # participants on the target. + def participants(target, user) + return unless target.respond_to?(:participants) + + target.participants(user).dup + end + # Get project/group users with CUSTOM notification level def add_custom_notifications(recipients, action) user_ids = [] diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index 315c3e16292..f385e426827 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -10,7 +10,7 @@ module Projects merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) merge_requests.each do |mr| - MergeRequests::CloseService.new(@project, @current_user).execute(mr) + ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end @project.forked_project_link.destroy diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 17cf71cf098..e60b854f916 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -93,10 +93,11 @@ module Projects end # Requires UnZip at least 6.00 Info-ZIP. + # -qq be (very) quiet # -n never overwrite existing files # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories site_path = File.join(SITE_PATH, '*') - unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path})) + unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path})) raise 'pages failed to extract' end end 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/destroy_service.rb b/app/services/users/destroy_service.rb index 673afb8b5b9..9d7237c2fbb 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -35,7 +35,7 @@ module Users Groups::DestroyService.new(group, current_user).execute end - user.personal_projects.with_deleted.each do |project| + user.personal_projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute 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/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/doorkeeper/applications/edit.html.haml b/app/views/doorkeeper/applications/edit.html.haml index fb6aa30acee..49f90298a50 100644 --- a/app/views/doorkeeper/applications/edit.html.haml +++ b/app/views/doorkeeper/applications/edit.html.haml @@ -1,3 +1,4 @@ - page_title "Edit", @application.name, "Applications" +- @content_class = "limit-container-width" unless fluid_layout %h3.page-title Edit application = render 'form', application: @application diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index aa271150b07..d1237d7bf6f 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -1,7 +1,8 @@ - page_title "Applications" +- @content_class = "limit-container-width" unless fluid_layout .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = page_title %p @@ -10,7 +11,7 @@ and applications that you've authorized to use your account. - else Manage applications that you've authorized to use your account. - .col-lg-9 + .col-lg-8 - if user_oauth_applications? %h5.prepend-top-0 Add new application diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 559de63d96d..72eab964766 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -1,4 +1,6 @@ - page_title @application.name, "Applications" +- @content_class = "limit-container-width" unless fluid_layout + %h3.page-title Application: #{@application.name} 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/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index eea33b5966f..cc710f4ec7d 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -11,6 +11,8 @@ %meta{ property: 'og:title', content: page_title } %meta{ property: 'og:description', content: page_description } %meta{ property: 'og:image', content: page_image } + %meta{ property: 'og:image:width', content: '64' } + %meta{ property: 'og:image:height', content: '64' } %meta{ property: 'og:url', content: request.base_url + request.fullpath } -# Twitter Card - https://dev.twitter.com/cards/types/summary @@ -30,6 +32,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/_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/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 969c423b032..4d41579168c 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,7 +1,11 @@ %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&.persisted? @@ -29,7 +33,7 @@ = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project) - if merge_project %li - = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project) + = link_to 'New merge request', namespace_project_new_merge_request_path(merge_project.namespace, merge_project) - if create_project_snippet %li.header-new-project-snippet = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project) 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..40c1ca7b53e --- /dev/null +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -0,0 +1,123 @@ +.nav-sidebar + = link_to admin_root_path, title: 'Admin Overview', class: 'context-header' do + .avatar-container.s40.settings-avatar + = icon('wrench') + .project-title Admin Area + %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..b7ac04cc3e5 --- /dev/null +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -0,0 +1,61 @@ +.nav-sidebar + = link_to group_path(@group), title: 'Group', class: 'context-header' do + .avatar-container.s40.group-avatar + = image_tag group_icon(@group), class: "avatar s40 avatar-tile" + .group-title + = @group.name + %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..033ea149cfb --- /dev/null +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -0,0 +1,53 @@ +.nav-sidebar + = link_to profile_path, title: 'Profile Settings', class: 'context-header' do + .avatar-container.s40.settings-avatar + = icon('user') + .project-title User Settings + %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..eae9da5da14 --- /dev/null +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -0,0 +1,247 @@ +.nav-sidebar + - can_edit = can?(current_user, :admin_project, @project) + = link_to project_path(@project), title: 'Project', class: 'context-header' do + .avatar-container.s40.project-avatar + = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') + .project-title + = @project.name + %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/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 29658da7792..68024d782a6 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -31,7 +31,9 @@ %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests - = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] + - controllers.push(:merge_requests, :labels, :milestones) unless @project.default_issues_tracker? + = nav_link(controller: controllers) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests 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/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a319b18e507..ed079ed7dfb 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,4 +1,5 @@ - page_title "Account" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' - if current_user.ldap_user? @@ -6,13 +7,13 @@ Some options are unavailable for LDAP accounts .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Private Tokens %p Keep these tokens secret, anyone with access to them can interact with GitLab as if they were you. - .col-lg-9.private-tokens-reset + .col-lg-8.private-tokens-reset = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' } = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' } @@ -22,12 +23,12 @@ %hr .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Two-Factor Authentication %p Increase your account's security by enabling Two-Factor Authentication (2FA). - .col-lg-9 + .col-lg-8 %p Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} - if current_user.two_factor_enabled? @@ -43,12 +44,12 @@ %hr - if button_based_providers.any? .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Social sign-in %p Activate signin with one of the following services - .col-lg-9 + .col-lg-8 %label.label-light Connected Accounts %p Click on icon to activate signin with one of the following services @@ -69,12 +70,12 @@ %hr - if current_user.can_change_username? .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0.warning-title Change username %p Changing your username will change path to all personal projects! - .col-lg-9 + .col-lg-8 = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f| .form-group = f.label :username, "Path", class: "label-light" @@ -93,10 +94,10 @@ - if signup_enabled? .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0.danger-title Remove account - .col-lg-9 + .col-lg-8 - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting an account has the following effects: diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index a24b7fd101d..1a392e29e2a 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,11 +1,12 @@ - page_title "Authentication log" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h3.prepend-top-0 = page_title %p This is a security log of important events involving your account. - .col-lg-9 + .col-lg-8 = render 'event_table', events: @events diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml index 20cc636b2da..8f7121afe02 100644 --- a/app/views/profiles/chat_names/index.html.haml +++ b/app/views/profiles/chat_names/index.html.haml @@ -1,14 +1,15 @@ - page_title 'Chat' +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = page_title %p You can see your Chat accounts. - .col-lg-9 + .col-lg-8 %h5 Active chat names (#{@chat_names.size}) - if @chat_names.present? diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index f5a323dbaf8..612ecbbb96a 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,13 +1,14 @@ - page_title "Emails" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = page_title %p Control emails linked to your account - .col-lg-9 + .col-lg-8 %h4.prepend-top-0 Add email address = form_for 'email', url: profile_emails_path do |f| diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 71b224a413b..5f7b41cf30e 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,13 +1,14 @@ - page_title "SSH Keys" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = page_title %p SSH keys allow you to establish a secure connection between your computer and GitLab. - .col-lg-9 + .col-lg-8 %h5.prepend-top-0 Add an SSH key %p.profile-settings-content diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml index 6283ceebf10..172c0450381 100644 --- a/app/views/profiles/keys/show.html.haml +++ b/app/views/profiles/keys/show.html.haml @@ -1,3 +1,4 @@ - page_title @key.title, "SSH Keys" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' = render "key_details" diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 51c4e8e5a73..e98fdfc7a3d 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,4 +1,5 @@ - page_title "Notifications" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' %div @@ -10,14 +11,14 @@ = hidden_field_tag :notification_type, 'global' .row - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4 = page_title %p You can specify notification level per group or per project. %p By default, all projects and groups will use the global notifications setting. - .col-lg-9 + .col-lg-8 %h5 Global notification settings diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 243428b690e..985bb79508f 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,12 +1,13 @@ - page_title "Password" +- @content_class = "limit-container-width" unless fluid_layout .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = page_title %p After a successful password update, you will be redirected to the login page where you can log in with your new password. - .col-lg-9 + .col-lg-8 %h5.prepend-top-0 Change your password - unless @user.password_automatically_set? diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index c852107e69a..cf750378e25 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -1,8 +1,9 @@ - page_title "Personal Access Tokens" +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = page_title %p @@ -11,7 +12,7 @@ You can also use personal access tokens to authenticate against Git over HTTP. They are the only accepted password when you have Two-Factor Authentication (2FA) enabled. - .col-lg-9 + .col-lg-8 - if flash[:personal_access_token] .created-personal-access-token-container diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 0ff19b3eab1..a089aeb2447 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,30 +1,50 @@ - page_title 'Preferences' +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Syntax highlighting theme %p This setting allows you to customize the appearance of the syntax. = succeed '.' do = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank' - .col-lg-9.syntax-theme + .col-lg-8.syntax-theme - Gitlab::ColorSchemes.each do |scheme| = label_tag do .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 + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Behavior %p This setting allows you to customize the behavior of the system layout and default views. = succeed '.' do = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank' - .col-lg-9 + .col-lg-8 .form-group = f.label :layout, class: 'label-light' do Layout width diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 819c98946ab..bac75a49075 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,10 +1,11 @@ +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' = 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 - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Public Avatar %p @@ -16,7 +17,7 @@ You can upload an avatar here - if gravatar_enabled? or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} - .col-lg-9 + .col-lg-8 .clearfix.avatar-image.append-bottom-default = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' @@ -34,14 +35,14 @@ = 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 + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Main settings %p This information will appear on your profile. - if current_user.ldap_user? Some options are unavailable for LDAP accounts - .col-lg-9 + .col-lg-8 .row = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: 'Enter your name, so people you know can recognize you.' diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 0ff05098cd7..67792de3870 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -1,5 +1,6 @@ - page_title 'Two-Factor Authentication', 'Account' - header_title "Two-Factor Authentication", profile_two_factor_auth_path +- @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' - if inject_u2f_api? @@ -7,12 +8,12 @@ = page_specific_javascript_bundle_tag('u2f') .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0 Register Two-Factor Authentication App %p Use an app on your mobile device to enable two-factor authentication (2FA). - .col-lg-9 + .col-lg-8 - if current_user.two_factor_otp_enabled? = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page." - else @@ -20,9 +21,9 @@ Download the Google Authenticator application from App Store or Google Play Store and scan this code. More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. .row.append-bottom-10 - .col-md-3 + .col-md-4 = raw @qr_code - .col-md-9 + .col-md-8 .account-well %p.prepend-top-0.append-bottom-0 Can't scan the code? @@ -50,7 +51,7 @@ .row.prepend-top-default - .col-lg-3 + .col-lg-4 %h4.prepend-top-0 Register Universal Two-Factor (U2F) Device %p @@ -59,7 +60,7 @@ As U2F devices are only supported by a few browsers, we require that you set up a two-factor authentication app before a U2F device. That way you'll always be able to log in - even when you're using an unsupported browser. - .col-lg-9 + .col-lg-8 - if @u2f_registration.errors.present? = form_errors(@u2f_registration) = render "u2f/register" diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index ce937ee1842..3627f72f5e1 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - project_duration = age_map_duration(@blame_groups, @project) -- page_title "Annotate", @blob.path, @ref +- page_title "Blame", @blob.path, @ref = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml index 5840e9863f4..2c944b516a4 100644 --- a/app/views/projects/blob/_breadcrumb.html.haml +++ b/app/views/projects/blob/_breadcrumb.html.haml @@ -27,7 +27,7 @@ = 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), diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 960b57a8008..aa1a533b5cb 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -16,7 +16,7 @@ - if merge_project %li - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do + = link_to namespace_project_new_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') #{ _('New merge request') } diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 11de6915961..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 diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 93fd0789c11..cf8dffc8957 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -8,7 +8,7 @@ %li.commits-row{ data: { day: day } } %ul.content-list.commit-list - = render commits, project: project, ref: ref + = render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref } - if hidden > 0 %li.alert.alert-warning diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index fabd825aec8..7ed7e441344 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -8,27 +8,28 @@ = render "head" %div{ class: container_class } - .row-content-block.second-block.content-component-block.flex-container-block - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'commits' + .tree-holder + .nav-block + .tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'commits' + + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + .tree-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' + - 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' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs - - .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' - - elsif create_mr_button?(@repository.root_ref, @ref) + = 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 } .control - = 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 } - .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do - = icon("rss") + = 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) } %ol#commits-list.list-unstyled.content_list diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index 26f3c297260..520696b01c6 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -11,11 +11,14 @@ .table-mobile-header{ role: 'rowheader' } Job - if deployment.deployable .table-mobile-content - = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do - #{deployment.deployable.name} (##{deployment.deployable.id}) + .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 - by - = user_avatar(user: deployment.user, size: 20) + %div + by + = user_avatar(user: deployment.user, size: 20) .table-section.section-15{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' } Created diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index d538c4c86c8..f9385459a66 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -10,7 +10,7 @@ - if show_whitespace_toggle - if current_controller?(:commit) = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') - - elsif current_controller?(:merge_requests) + - elsif current_controller?('projects/merge_requests/diffs') = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs') - elsif current_controller?(:compare) = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs') diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 43708d22a0c..cd0fb21f8a7 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -19,6 +19,7 @@ - if plain = link_text - else + = add_diff_note_button(line_code, diff_file.position(line), type) %a{ href: "##{line_code}", data: { linenumber: link_text } } - discussion = line_discussions.try(:first) - if discussion && discussion.resolvable? && !plain @@ -29,7 +30,7 @@ = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } - %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< + %td.line_content.noteable_line{ class: type }< - if email %pre= line.text - else diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 8e5f4d2573d..56d63250714 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,4 +1,5 @@ / Side-by-side diff view + .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - diff_file.parallel_diff_lines.each do |line| @@ -18,11 +19,12 @@ - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + = add_diff_note_button(left_line_code, left_position, 'old') %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - discussion_left = discussions_left.try(:first) - if discussion_left && discussion_left.resolvable? %diff-note-avatars{ "discussion-id" => discussion_left.id } - %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) + %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel @@ -38,11 +40,12 @@ - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + = add_diff_note_button(right_line_code, right_position, 'new') %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - discussion_right = discussions_right.try(:first) - if discussion_right && discussion_right.resolvable? %diff-note-avatars{ "discussion-id" => discussion_right.id } - %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) + %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 295a1b62535..402c18c447e 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -2,13 +2,12 @@ %h4 Too many changes to show. .pull-right - - if current_controller?(:commit) or current_controller?(:merge_requests) - - if current_controller?(:commit) - = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm" - = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-sm" - - elsif @merge_request && @merge_request.persisted? - = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-sm" - = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm" + - if current_controller?(:commit) + = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm" + = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-sm" + - elsif current_controller?('projects/merge_requests/diffs') && @merge_request&.persisted? + = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-sm" + = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm" %p To preserve performance only %strong #{diff_files.size} of #{diff_files.real_size} diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index e8f8fbbcf09..c5722cf5997 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -1,11 +1,12 @@ - @no_container = true - page_title "Metrics for environment", @environment.name - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_d3') - = page_specific_javascript_bundle_tag('monitoring') + = webpack_bundle_tag 'common_vue' + = webpack_bundle_tag 'common_d3' + = webpack_bundle_tag 'monitoring' = render "projects/pipelines/head" -#js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } } +.prometheus-container{ class: container_class } .top-area .row .col-sm-6 @@ -13,68 +14,8 @@ Environment: = link_to @environment.name, environment_path(@environment) - .prometheus-state - .js-getting-started.hidden - .row - .col-md-4.col-md-offset-4.state-svg - = render "shared/empty_states/monitoring/getting_started.svg" - .row - .col-md-6.col-md-offset-3 - %h4.text-center.state-title - Get started with performance monitoring - .row - .col-md-6.col-md-offset-3 - .description-text.text-center.state-description - Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. - = link_to help_page_path('administration/monitoring/prometheus/index.md') do - Learn more about performance monitoring - .row.state-button-section - .col-md-4.col-md-offset-4.text-center.state-button - = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), class: 'btn btn-success' do - Configure Prometheus - .js-loading.hidden - .row - .col-md-4.col-md-offset-4.state-svg - = render "shared/empty_states/monitoring/loading.svg" - .row - .col-md-6.col-md-offset-3 - %h4.text-center.state-title - Waiting for performance data - .row - .col-md-6.col-md-offset-3 - .description-text.text-center.state-description - Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available. - .row.state-button-section - .col-md-4.col-md-offset-4.text-center.state-button - = link_to help_page_path('administration/monitoring/prometheus/index.md'), class: 'btn btn-success' do - View documentation - .js-unable-to-connect.hidden - .row - .col-md-4.col-md-offset-4.state-svg - = render "shared/empty_states/monitoring/unable_to_connect.svg" - .row - .col-md-6.col-md-offset-3 - %h4.text-center.state-title - Unable to connect to Prometheus server - .row - .col-md-6.col-md-offset-3 - .description-text.text-center.state-description - Ensure connectivity is available from the GitLab server to the - = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus') do - Prometheus server - .row.state-button-section - .col-md-4.col-md-offset-4.text-center.state-button - = link_to help_page_path('administration/monitoring/prometheus/index.md'), class:'btn btn-success' do - View documentation + #prometheus-graphs{ data: { "settings-path": edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), + "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), + "additional-metrics": additional_metrics_namespace_project_environment_path(@project.namespace, @project, @environment, format: :json), + "has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } } - .prometheus-graphs - .row - .col-sm-12 - %h4 - CPU utilization - %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } - .row - .col-sm-12 - %h4 - Memory usage - %svg.prometheus-graph{ 'graph-type' => 'memory_values' } 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/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 9e4e6934ca9..6a0d96f50cd 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -4,43 +4,49 @@ .issue-check.hidden = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" .issue-info-container - .issue-title.title - %span.issue-title-text - = confidential_icon(issue) - = link_to issue.title, issue_path(issue) + .issue-main-info + .issue-title.title + %span.issue-title-text + = confidential_icon(issue) + = link_to issue.title, issue_path(issue) + - if issue.tasks? + %span.task-status.hidden-xs + + = issue.task_status + + .issuable-info + %span.issuable-reference + #{issuable_reference(issue)} + %span.issuable-authored.hidden-xs + · + opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} + by #{link_to_member(@project, issue.author, avatar: false)} + - if issue.milestone + %span.issuable-milestone.hidden-xs + + = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do + = icon('clock-o') + = issue.milestone.title + - if issue.due_date + %span.issuable-due-date.hidden-xs{ class: "#{'cred' if issue.overdue?}" } + + = icon('calendar') + = issue.due_date.to_s(:medium) + - if issue.labels.any? + + - issue.labels.each do |label| + = link_to_label(label, subject: issue.project, css_class: 'label-link') + + .issuable-meta %ul.controls - if issue.closed? - %li + %li.issuable-status CLOSED - - if issue.assignees.any? %li = render 'shared/issuable/assignees', project: @project, issue: issue = render 'shared/issuable_meta_data', issuable: issue - .issue-info - #{issuable_reference(issue)} · - opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} - by #{link_to_member(@project, issue.author, avatar: false)} - - if issue.milestone - - = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do - = icon('clock-o') - = issue.milestone.title - - if issue.due_date - %span{ class: "#{'cred' if issue.overdue?}" } - - = icon('calendar') - = issue.due_date.to_s(:medium) - - if issue.labels.any? - - - issue.labels.each do |label| - = link_to_label(label, subject: issue.project, css_class: 'label-link') - - if issue.tasks? - - %span.task-status - = issue.task_status - - .pull-right.issue-updated-at + .pull-right.issuable-updated-at.hidden-xs %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} 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/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/_commits.html.haml index 11793919ff7..11793919ff7 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/_commits.html.haml diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 766cb272bec..766cb272bec 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index c13110deb16..3599f2271b5 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -4,58 +4,60 @@ = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" .issue-info-container - .merge-request-title.title - %span.merge-request-title-text - = link_to merge_request.title, merge_request_path(merge_request) + .issue-main-info + .merge-request-title.title + %span.merge-request-title-text + = link_to merge_request.title, merge_request_path(merge_request) + - if merge_request.tasks? + %span.task-status.hidden-xs + + = merge_request.task_status + + .issuable-info + %span.issuable-reference + #{issuable_reference(merge_request)} + %span.issuable-authored.hidden-xs + · + opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} + by #{link_to_member(@project, merge_request.author, avatar: false)} + - if merge_request.milestone + %span.issuable-milestone.hidden-xs + + = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do + = icon('clock-o') + = merge_request.milestone.title + - if merge_request.target_project.default_branch != merge_request.target_branch + %span.project-ref-path + + = link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do + = icon('code-fork') + = merge_request.target_branch + - if merge_request.labels.any? + + - merge_request.labels.each do |label| + = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link') + + .issuable-meta %ul.controls - if merge_request.merged? - %li + %li.issuable-status.hidden-xs MERGED - elsif merge_request.closed? - %li + %li.issuable-status.hidden-xs = icon('ban') CLOSED - - if merge_request.head_pipeline - %li + %li.issuable-pipeline-status.hidden-xs = render_pipeline_status(merge_request.head_pipeline) - - if merge_request.open? && merge_request.broken? - %li + %li.issuable-pipeline-broken.hidden-xs = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do = icon('exclamation-triangle') - - if merge_request.assignee %li = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") = render 'shared/issuable_meta_data', issuable: merge_request - .merge-request-info - #{issuable_reference(merge_request)} · - opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} - by #{link_to_member(@project, merge_request.author, avatar: false)} - - if merge_request.target_project.default_branch != merge_request.target_branch - - = link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do - = icon('code-fork') - = merge_request.target_branch - - - if merge_request.milestone - - = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do - = icon('clock-o') - = merge_request.milestone.title - - - if merge_request.labels.any? - - - merge_request.labels.each do |label| - = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link') - - - if merge_request.tasks? - - %span.task-status - = merge_request.task_status - - .pull-right.hidden-xs + .pull-right.issuable-updated-at.hidden-xs %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')} diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml index 8a390cf8700..8a390cf8700 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/_mr_box.html.haml diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index d9428b8562e..d9428b8562e 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/_pipelines.html.haml index 2f1dbe87619..2f1dbe87619 100644 --- a/app/views/projects/merge_requests/show/_pipelines.html.haml +++ b/app/views/projects/merge_requests/_pipelines.html.haml diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml deleted file mode 100644 index 75120409bb3..00000000000 --- a/app/views/projects/merge_requests/_show.html.haml +++ /dev/null @@ -1,97 +0,0 @@ -- @content_class = "limit-container-width" unless fluid_layout -- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" -- page_description @merge_request.description -- page_card_attributes @merge_request.card_attributes -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('diff_notes') - -.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } - = render "projects/merge_requests/show/mr_title" - - .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } - = render "projects/merge_requests/show/mr_box" - - - if @merge_request.source_branch_exists? - = render "projects/merge_requests/show/how_to_merge" - - :javascript - window.gl.mrWidgetData = #{serialize_issuable(@merge_request)} - - #js-vue-mr-widget.mr-widget - - - content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'vue_merge_request_widget' - - .content-block.content-block-small.emoji-list-container - = render 'award_emoji/awards_block', awardable: @merge_request, inline: true - - .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - .merge-request-tabs-container - .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller - .fade-left= icon('angle-left') - .fade-right= icon('angle-right') - .nav-links.scrolling-tabs - %ul.merge-request-tabs - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do - Discussion - %span.badge= @merge_request.related_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count - - if @pipelines.any? - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @pipelines.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } - %div - .line-resolve-all{ "v-show" => "discussionCount > 0", - ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } - %span.line-resolve-btn.is-disabled{ type: "button", - ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } - = render "shared/icons/icon_status_success.svg" - %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved - = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request - = render "discussions/jump_to_next" - - .tab-content#diff-notes-app - #notes.notes.tab-pane.voting_notes - .row - %section.col-md-12 - .issuable-discussion - = render "projects/merge_requests/discussion" - - #commits.commits.tab-pane - -# This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - - if @pipelines.any? - = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - #diffs.diffs.tab-pane - -# This tab is always loaded via AJAX - - .mr-loading-status - = spinner - -= render 'shared/issuable/sidebar', issuable: @merge_request -- if @merge_request.can_be_reverted?(current_user) - = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title -- if @merge_request.can_be_cherry_picked? - = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title - -:javascript - $(function () { - window.mergeRequest = new MergeRequest({ - action: "#{controller.action_name}" - }); - }); diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 51d59280be8..f016b9c13b3 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -3,10 +3,10 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') -= render "projects/merge_requests/show/mr_title" += render "projects/merge_requests/mr_title" .merge-request-details.issuable-details - = render "projects/merge_requests/show/mr_box" + = render "projects/merge_requests/mr_box" = render 'shared/issuable/sidebar', issuable: @merge_request diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml new file mode 100644 index 00000000000..f016b9c13b3 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/show.html.haml @@ -0,0 +1,38 @@ +- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('merge_conflicts') + = page_specific_javascript_tag('lib/ace.js') += render "projects/merge_requests/mr_title" + +.merge-request-details.issuable-details + = render "projects/merge_requests/mr_box" + += render 'shared/issuable/sidebar', issuable: @merge_request + +#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json), + resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } } + .loading{ "v-if" => "isLoading" } + %i.fa.fa-spinner.fa-spin + + .nothing-here-block{ "v-if" => "hasError" } + {{conflictsData.errorMessage}} + + = render partial: "projects/merge_requests/conflicts/commit_stats" + + .files-wrapper{ "v-if" => "!isLoading && !hasError" } + .files + .diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" } + .js-file-title.file-title + %i.fa.fa-fw{ ":class" => "file.iconClass" } + %strong {{file.filePath}} + = render partial: 'projects/merge_requests/conflicts/file_actions' + .diff-content.diff-wrap-lines + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines" + .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + %parallel-conflict-lines{ ":file" => "file" } + %div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" } + = render partial: "projects/merge_requests/conflicts/components/diff_file_editor" + + = render partial: "projects/merge_requests/conflicts/submit_form" diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/creations/_diffs.html.haml index 627fc4e9671..627fc4e9671 100644 --- a/app/views/projects/merge_requests/_new_diffs.html.haml +++ b/app/views/projects/merge_requests/creations/_diffs.html.haml diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 0f37abb579c..7cda326afef 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -1,7 +1,7 @@ %h3.page-title New Merge Request -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: namespace_project_new_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors .merge-request-branches.row .col-md-6 @@ -69,7 +69,7 @@ :javascript new Compare({ - targetProjectUrl: "#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", - sourceBranchUrl: "#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", - targetBranchUrl: "#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}" + targetProjectUrl: "#{namespace_project_new_merge_request_update_branches_path(@source_project.namespace, @source_project)}", + sourceBranchUrl: "#{namespace_project_new_merge_request_branch_from_path(@source_project.namespace, @source_project)}", + targetBranchUrl: "#{namespace_project_new_merge_request_branch_to_path(@source_project.namespace, @source_project)}" }); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index e3ecbee5490..c72dd1d8e29 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -31,28 +31,27 @@ %span.badge= @commits.size - if @pipelines.any? %li.builds-tab - = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do + = link_to url_for(params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines %span.badge= @pipelines.size %li.diffs-tab - = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do + = link_to url_for(params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @merge_request.diff_size .tab-content #commits.commits.tab-pane.active - = render "projects/merge_requests/show/commits" + = render "projects/merge_requests/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane - = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json)), disable_initialization: true + = render 'projects/merge_requests/pipelines', endpoint: url_for(params.merge(action: 'pipelines', format: :json)), disable_initialization: true .mr-loading-status = spinner :javascript var merge_request = new MergeRequest({ - action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - setUrl: false, + action: "#{j params[:tab].presence || 'new'}", }); diff --git a/app/views/projects/merge_requests/branch_from.html.haml b/app/views/projects/merge_requests/creations/branch_from.html.haml index 3837c4b388d..3837c4b388d 100644 --- a/app/views/projects/merge_requests/branch_from.html.haml +++ b/app/views/projects/merge_requests/creations/branch_from.html.haml diff --git a/app/views/projects/merge_requests/branch_to.html.haml b/app/views/projects/merge_requests/creations/branch_to.html.haml index d69b71790a0..d69b71790a0 100644 --- a/app/views/projects/merge_requests/branch_to.html.haml +++ b/app/views/projects/merge_requests/creations/branch_to.html.haml diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/creations/new.html.haml index 2e798ce780a..2e798ce780a 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/creations/new.html.haml diff --git a/app/views/projects/merge_requests/update_branches.html.haml b/app/views/projects/merge_requests/creations/update_branches.html.haml index 64482973a89..64482973a89 100644 --- a/app/views/projects/merge_requests/update_branches.html.haml +++ b/app/views/projects/merge_requests/creations/update_branches.html.haml diff --git a/app/views/projects/merge_requests/diffs.html.haml b/app/views/projects/merge_requests/diffs.html.haml deleted file mode 100644 index 2a5b8b1441e..00000000000 --- a/app/views/projects/merge_requests/diffs.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "show" diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml index 7f0913ea516..fb31e2fef00 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? || @merge_request_diff.overflow? - = render 'projects/merge_requests/show/versions' + = render 'projects/merge_requests/diffs/versions' = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/diffs/_versions.html.haml index 0999b95c9c9..0999b95c9c9 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/diffs/_versions.html.haml diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 6d75a9f34a3..1e30cc09894 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -13,6 +13,9 @@ = render 'projects/last_push' +- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project + - if @project.merge_requests.exists? %div{ class: container_class } .top-area @@ -20,9 +23,8 @@ .nav-controls - if @can_bulk_update = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - if merge_project - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do + = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do New merge request = render 'shared/issuable/search_bar', type: :merge_requests @@ -33,4 +35,4 @@ .merge-requests-holder = render 'merge_requests' - else - = render 'shared/empty_states/merge_requests', button_path: new_namespace_project_merge_request_path(@project.namespace, @project) + = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index a00d3128ffe..6df19d6438b 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,8 +1,8 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" .merge-request - = render "projects/merge_requests/show/mr_title" - = render "projects/merge_requests/show/mr_box" + = render "projects/merge_requests/mr_title" + = render "projects/merge_requests/mr_box" .alert.alert-danger %p diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 2a5b8b1441e..dbbf1bde088 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -1 +1,97 @@ -= render "show" +- @content_class = "limit-container-width" unless fluid_layout +- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" +- page_description @merge_request.description +- page_card_attributes @merge_request.card_attributes +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('diff_notes') + +.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } + = render "projects/merge_requests/mr_title" + + .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } + = render "projects/merge_requests/mr_box" + + - if @merge_request.source_branch_exists? + = render "projects/merge_requests/how_to_merge" + + :javascript + window.gl.mrWidgetData = #{serialize_issuable(@merge_request)} + + #js-vue-mr-widget.mr-widget + + - content_for :page_specific_javascripts do + = webpack_bundle_tag 'common_vue' + = webpack_bundle_tag 'vue_merge_request_widget' + + .content-block.content-block-small.emoji-list-container + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true + + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + .merge-request-tabs-container + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + %ul.merge-request-tabs + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'show', toggle: 'tab' } do + Discussion + %span.badge= @merge_request.related_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + %div + .line-resolve-all{ "v-show" => "discussionCount > 0", + ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } + %span.line-resolve-btn.is-disabled{ type: "button", + ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } + = render "shared/icons/icon_status_success.svg" + %span.line-resolve-text + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved + = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request + = render "discussions/jump_to_next" + + .tab-content#diff-notes-app + #notes.notes.tab-pane.voting_notes + .row + %section.col-md-12 + .issuable-discussion + = render "projects/merge_requests/discussion" + + #commits.commits.tab-pane + -# This tab is always loaded via AJAX + #pipelines.pipelines.tab-pane + - if @pipelines.any? + = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + #diffs.diffs.tab-pane + -# This tab is always loaded via AJAX + + .mr-loading-status + = spinner + += render 'shared/issuable/sidebar', issuable: @merge_request +- if @merge_request.can_be_reverted?(current_user) + = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_cherry_picked? + = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title + +:javascript + $(function () { + window.mergeRequest = new MergeRequest({ + action: "#{j params[:tab].presence || 'show'}", + }); + }); 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/show.html.haml b/app/views/projects/show.html.haml index 7447197ed89..e1e70a53709 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -21,7 +21,7 @@ %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do #{n_('Commit', 'Commits', @project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %l + %li = link_to namespace_project_branches_path(@project.namespace, @project) do #{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) %li diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 1d4fd71522d..435acbc634c 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -5,21 +5,21 @@ - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - if issuable_mr > 0 - %li + %li.issuable-mr.hidden-xs = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = issuable_mr - if upvotes > 0 - %li + %li.issuable-upvotes.hidden-xs = icon('thumbs-up') = upvotes - if downvotes > 0 - %li + %li.issuable-downvotes.hidden-xs = icon('thumbs-down') = downvotes -%li +%li.issuable-comments.hidden-xs = link_to issuable_url, class: ('no-comments' if note_count.zero?) do = icon('comments') = note_count 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/_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/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index a212c714826..785a500e44e 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,3 +1,5 @@ +- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues' + .dropdown.inline.prepend-left-10 %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } } - if @sort.present? @@ -23,7 +25,7 @@ = sort_title_milestone_soon = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do = sort_title_milestone_later - - if controller.controller_name == 'issues' || controller.action_name == 'issues' + - if viewing_issues = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do = sort_title_due_date_soon = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do 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/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/_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/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index aaffc0927eb..7ed6c622558 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -13,7 +13,7 @@ - if projects.any? %ul.projects-list - projects.each_with_index do |project, i| - - css_class = (i >= projects_limit) ? 'hide' : nil + - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, forks: forks, show_last_commit_as_description: show_last_commit_as_description 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/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index d760f5b140f..92e622285de 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -46,7 +46,7 @@ class ExpirePipelineCacheWorker end def new_merge_request_pipelines_path(project) - Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path( + Gitlab::Routing.url_helpers.namespace_project_new_merge_request_path( project.namespace, project, format: :json) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 89286595ca6..b8f8d3750d9 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -2,11 +2,11 @@ class PostReceive include Sidekiq::Worker include DedicatedSidekiqQueue - def perform(project_identifier, identifier, changes) - project, is_wiki = parse_project_identifier(project_identifier) + def perform(gl_repository, identifier, changes) + project, is_wiki = Gitlab::GlRepository.parse(gl_repository) if project.nil? - log("Triggered hook for non-existing project with identifier \"#{project_identifier}\"") + log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"") return false end @@ -59,21 +59,6 @@ class PostReceive # Nothing defined here yet. end - # To maintain backwards compatibility, we accept both gl_repository or - # repository paths as project identifiers. Our plan is to migrate to - # gl_repository only with the following plan: - # 9.2: Handle both possible values. Keep Gitlab-Shell sending only repo paths - # 9.3 (or patch release): Make GitLab Shell pass gl_repository if present - # 9.4 (or patch release): Make GitLab Shell always pass gl_repository - # 9.5 (or patch release): Handle only gl_repository as project identifier on this method - def parse_project_identifier(project_identifier) - if project_identifier.start_with?('/') - Gitlab::RepoPath.parse(project_identifier) - else - Gitlab::GlRepository.parse(project_identifier) - end - end - def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index ae8c980c9e4..8b0cfcc8af8 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -45,7 +45,7 @@ class StuckCiJobsWorker def search(status, timeout) builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago) - builds.joins(:project).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build| + builds.joins(:project).merge(Project.without_deleted).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build| yield(build) end end 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/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/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/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/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/26125-match-username-on-search.yml b/changelogs/unreleased/26125-match-username-on-search.yml new file mode 100644 index 00000000000..74e918bec16 --- /dev/null +++ b/changelogs/unreleased/26125-match-username-on-search.yml @@ -0,0 +1,5 @@ +--- +title: Inserts exact matches of name, username and email to the top of the search + list +merge_request: 12525 +author: 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/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/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/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/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/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/30708-stop-using-deleted-at-to-filter-namespaces.yml b/changelogs/unreleased/30708-stop-using-deleted-at-to-filter-namespaces.yml new file mode 100644 index 00000000000..83ce3fb4d0a --- /dev/null +++ b/changelogs/unreleased/30708-stop-using-deleted-at-to-filter-namespaces.yml @@ -0,0 +1,4 @@ +--- +title: Removes deleted_at and pending_delete occurrences in Project related queries +merge_request: 12091 +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/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/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/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/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/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/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/33082-use-update_pipeline_schedule-for-edit-and-take_ownership-in-pipelineschedulescontroller.yml b/changelogs/unreleased/33082-use-update_pipeline_schedule-for-edit-and-take_ownership-in-pipelineschedulescontroller.yml new file mode 100644 index 00000000000..d3172c405c3 --- /dev/null +++ b/changelogs/unreleased/33082-use-update_pipeline_schedule-for-edit-and-take_ownership-in-pipelineschedulescontroller.yml @@ -0,0 +1,4 @@ +--- +title: Use authorize_update_pipeline_schedule in PipelineSchedulesController +merge_request: 11846 +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/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/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml b/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml new file mode 100644 index 00000000000..d6b1b2524c6 --- /dev/null +++ b/changelogs/unreleased/33443-supplement_traditional_chinese_in_taiwan_translation_of_i18n.yml @@ -0,0 +1,4 @@ +--- +title: Supplement Traditional Chinese in Taiwan translation of Project Page & Repository Page +merge_request: 12514 +author: Huang Tao 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/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/33878_fix_edit_deploy_key.yml b/changelogs/unreleased/33878_fix_edit_deploy_key.yml deleted file mode 100644 index bc47d522240..00000000000 --- a/changelogs/unreleased/33878_fix_edit_deploy_key.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix edit button for deploy keys available from other projects -merge_request: 12301 -author: Alexander Randa diff --git a/changelogs/unreleased/33917-mr-comment-system-note-highlight-don-t-have-the-same-width.yml b/changelogs/unreleased/33917-mr-comment-system-note-highlight-don-t-have-the-same-width.yml deleted file mode 100644 index f3b92878f6d..00000000000 --- a/changelogs/unreleased/33917-mr-comment-system-note-highlight-don-t-have-the-same-width.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Standardize timeline note margins across different viewport sizes -merge_request: 12364 -author: diff --git a/changelogs/unreleased/34008-fix-CI_ENVIRONMENT_URL-2.yml b/changelogs/unreleased/34008-fix-CI_ENVIRONMENT_URL-2.yml deleted file mode 100644 index 7f4d6e3bc67..00000000000 --- a/changelogs/unreleased/34008-fix-CI_ENVIRONMENT_URL-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix passing CI_ENVIRONMENT_NAME and CI_ENVIRONMENT_SLUG for CI_ENVIRONMENT_URL -merge_request: 12344 -author: diff --git a/changelogs/unreleased/34010-fix-linking-to-parallel-diff-line-number-creating-gray-box.yml b/changelogs/unreleased/34010-fix-linking-to-parallel-diff-line-number-creating-gray-box.yml deleted file mode 100644 index 705d44f8b10..00000000000 --- a/changelogs/unreleased/34010-fix-linking-to-parallel-diff-line-number-creating-gray-box.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix linking to line number on side-by-side diff creating empty discussion box -merge_request: -author: 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/34078-allow-to-enable-feature-flags-with-more-granularity.yml b/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml new file mode 100644 index 00000000000..69d5d34b072 --- /dev/null +++ b/changelogs/unreleased/34078-allow-to-enable-feature-flags-with-more-granularity.yml @@ -0,0 +1,4 @@ +--- +title: Allow the feature flags to be enabled/disabled with more granularity +merge_request: 12357 +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/34403-issue-dropdown-persists-when-adding-issue-number-to-issue-description.yml b/changelogs/unreleased/34403-issue-dropdown-persists-when-adding-issue-number-to-issue-description.yml new file mode 100644 index 00000000000..4911315d018 --- /dev/null +++ b/changelogs/unreleased/34403-issue-dropdown-persists-when-adding-issue-number-to-issue-description.yml @@ -0,0 +1,4 @@ +--- +title: Closes any open Autocomplete of the markdown editor when the form is closed +merge_request: 12521 +author: 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-group-members-counting-and-plan-related-data-on-namespaces-api.yml b/changelogs/unreleased/add-group-members-counting-and-plan-related-data-on-namespaces-api.yml new file mode 100644 index 00000000000..f2591042e98 --- /dev/null +++ b/changelogs/unreleased/add-group-members-counting-and-plan-related-data-on-namespaces-api.yml @@ -0,0 +1,4 @@ +--- +title: Add group members counting and plan related data on namespaces API +merge_request: +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-all-reserved-paths.yml b/changelogs/unreleased/bvl-rename-all-reserved-paths.yml new file mode 100644 index 00000000000..f37f2fa94ae --- /dev/null +++ b/changelogs/unreleased/bvl-rename-all-reserved-paths.yml @@ -0,0 +1,4 @@ +--- +title: Rename all reserved paths that could have been created +merge_request: 11713 +author: 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/disable-blocked-manual-actions.yml b/changelogs/unreleased/disable-blocked-manual-actions.yml deleted file mode 100644 index a640f61a7dd..00000000000 --- a/changelogs/unreleased/disable-blocked-manual-actions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: disable blocked manual actions -merge_request: -author: diff --git a/changelogs/unreleased/disable-environment-list-refresh.yml b/changelogs/unreleased/disable-environment-list-refresh.yml deleted file mode 100644 index 62fd71496a0..00000000000 --- a/changelogs/unreleased/disable-environment-list-refresh.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable environment list refresh due to bug https://gitlab.com/gitlab-org/gitlab-ee/issues/2677 -merge_request: 12347 -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-dependency-linker-newlines.yml b/changelogs/unreleased/dm-dependency-linker-newlines.yml new file mode 100644 index 00000000000..5631095fcb7 --- /dev/null +++ b/changelogs/unreleased/dm-dependency-linker-newlines.yml @@ -0,0 +1,5 @@ +--- +title: Fix diff of requirements.txt file by not matching newlines as part of package + names +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-empty-state-new-merge-request.yml b/changelogs/unreleased/dm-empty-state-new-merge-request.yml new file mode 100644 index 00000000000..5fad7a0f883 --- /dev/null +++ b/changelogs/unreleased/dm-empty-state-new-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Fix 'New merge request' button for users who don't have push access to canonical + project +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-fix-parser-cache.yml b/changelogs/unreleased/dm-fix-parser-cache.yml deleted file mode 100644 index 31c163b7272..00000000000 --- a/changelogs/unreleased/dm-fix-parser-cache.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't return nil for missing objects from parser cache -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-page-image-size.yml b/changelogs/unreleased/dm-page-image-size.yml new file mode 100644 index 00000000000..b18c00470fc --- /dev/null +++ b/changelogs/unreleased/dm-page-image-size.yml @@ -0,0 +1,4 @@ +--- +title: Limit OpenGraph image size to 64x64 +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-relative-submodule-url-trailing-whitespace.yml b/changelogs/unreleased/dm-relative-submodule-url-trailing-whitespace.yml new file mode 100644 index 00000000000..616241dd941 --- /dev/null +++ b/changelogs/unreleased/dm-relative-submodule-url-trailing-whitespace.yml @@ -0,0 +1,4 @@ +--- +title: Strip trailing whitespace in relative submodule URL +merge_request: +author: diff --git a/changelogs/unreleased/dm-requirements-txt-tilde.yml b/changelogs/unreleased/dm-requirements-txt-tilde.yml deleted file mode 100644 index ddb5325ddd5..00000000000 --- a/changelogs/unreleased/dm-requirements-txt-tilde.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't match tilde and exclamation mark as part of requirements.txt package - name -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-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/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/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-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/fix-33259.yml b/changelogs/unreleased/fix-33259.yml deleted file mode 100644 index c68e42c02cf..00000000000 --- a/changelogs/unreleased/fix-33259.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix GitHub importer performance on branch existence check -merge_request: -author: 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-34417.yml b/changelogs/unreleased/fix-34417.yml new file mode 100644 index 00000000000..5f012ad0c81 --- /dev/null +++ b/changelogs/unreleased/fix-34417.yml @@ -0,0 +1,4 @@ +--- +title: Perform housekeeping only when an import of a fresh project is completed +merge_request: +author: diff --git a/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml b/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml 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-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-sidebar-showing-mobile-merge-requests.yml b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml new file mode 100644 index 00000000000..856990a6126 --- /dev/null +++ b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml @@ -0,0 +1,4 @@ +--- +title: Fixed sidebar not collapsing on merge requests in mobile screens +merge_request: +author: diff --git a/changelogs/unreleased/fix-support-for-external-ci-services.yml b/changelogs/unreleased/fix-support-for-external-ci-services.yml deleted file mode 100644 index eecb4519259..00000000000 --- a/changelogs/unreleased/fix-support-for-external-ci-services.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix support for external CI services -merge_request: 11176 -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/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/hb-fix-abuse-report-on-stale-user-profile.yml b/changelogs/unreleased/hb-fix-abuse-report-on-stale-user-profile.yml new file mode 100644 index 00000000000..ec2f4f9c3d8 --- /dev/null +++ b/changelogs/unreleased/hb-fix-abuse-report-on-stale-user-profile.yml @@ -0,0 +1,4 @@ +--- +title: Fix errors caused by attempts to report already blocked or deleted users +merge_request: 12502 +author: Horacio Bertorello diff --git a/changelogs/unreleased/highest-return-on-diff-investment.yml b/changelogs/unreleased/highest-return-on-diff-investment.yml new file mode 100644 index 00000000000..c8be1e0ff8f --- /dev/null +++ b/changelogs/unreleased/highest-return-on-diff-investment.yml @@ -0,0 +1,4 @@ +--- +title: Bring back branches badge to main project page +merge_request: 12548 +author: diff --git a/changelogs/unreleased/instrument-merge-request-diff-load-commits.yml b/changelogs/unreleased/instrument-merge-request-diff-load-commits.yml deleted file mode 100644 index 916b182a48b..00000000000 --- a/changelogs/unreleased/instrument-merge-request-diff-load-commits.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Instrument MergeRequestDiff#load_commits -merge_request: -author: 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_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/issueable-list-cleanup.yml b/changelogs/unreleased/issueable-list-cleanup.yml new file mode 100644 index 00000000000..d3d67d04574 --- /dev/null +++ b/changelogs/unreleased/issueable-list-cleanup.yml @@ -0,0 +1,4 @@ +--- +title: Clean up UI of issuable lists and make more responsive +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/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-breadcrumb-order-33938.yml b/changelogs/unreleased/mk-fix-breadcrumb-order-33938.yml deleted file mode 100644 index 790af692b92..00000000000 --- a/changelogs/unreleased/mk-fix-breadcrumb-order-33938.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix reversed breadcrumb order for nested groups -merge_request: 12322 -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/mk-fix-issue-34068.yml b/changelogs/unreleased/mk-fix-issue-34068.yml deleted file mode 100644 index af0a9139568..00000000000 --- a/changelogs/unreleased/mk-fix-issue-34068.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix 500 when failing to create private group -merge_request: 12394 -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/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/reduce-sidekiq-wait-timings.yml b/changelogs/unreleased/reduce-sidekiq-wait-timings.yml deleted file mode 100644 index 4d23accc82e..00000000000 --- a/changelogs/unreleased/reduce-sidekiq-wait-timings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reduce time spent waiting for certain Sidekiq jobs to complete -merge_request: -author: diff --git a/changelogs/unreleased/refactor-projects-finder-init-collection.yml b/changelogs/unreleased/refactor-projects-finder-init-collection.yml deleted file mode 100644 index c8113419f21..00000000000 --- a/changelogs/unreleased/refactor-projects-finder-init-collection.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactor ProjectsFinder#init_collection to produce more efficient queries for - retrieving projects -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/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-project-destroy-in-namespace.yml b/changelogs/unreleased/sh-fix-project-destroy-in-namespace.yml new file mode 100644 index 00000000000..9309f961345 --- /dev/null +++ b/changelogs/unreleased/sh-fix-project-destroy-in-namespace.yml @@ -0,0 +1,4 @@ +--- +title: Defer project destroys within a namespace in Groups::DestroyService#async_execute +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/stop-notification-recipient-service-modifying-participants.yml b/changelogs/unreleased/stop-notification-recipient-service-modifying-participants.yml new file mode 100644 index 00000000000..7e66ea4ca8b --- /dev/null +++ b/changelogs/unreleased/stop-notification-recipient-service-modifying-participants.yml @@ -0,0 +1,5 @@ +--- +title: Ensure participants for issues, merge requests, etc. are calculated correctly + when sending notifications +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/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-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-raise-etag-route-regex-miss.yml b/changelogs/unreleased/zj-raise-etag-route-regex-miss.yml deleted file mode 100644 index 57a5f4e44c0..00000000000 --- a/changelogs/unreleased/zj-raise-etag-route-regex-miss.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix etag route not being a match for environments -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/changelogs/unreleased/zj-usage-ping-only-gl-pipelines.yml b/changelogs/unreleased/zj-usage-ping-only-gl-pipelines.yml new file mode 100644 index 00000000000..0ace7b99657 --- /dev/null +++ b/changelogs/unreleased/zj-usage-ping-only-gl-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Split pipelines as internal and external in the usage data +merge_request: 12277 +author: 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 2d01092acd5..02baeab29ab 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -5,6 +5,12 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +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' diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index 2bd159ca7f1..482613dacc9 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -1,6 +1,8 @@ -required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) -current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) +unless Rails.env.test? + required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) + current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) -unless current_version.valid? && required_version <= current_version - warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell." + unless current_version.valid? && required_version <= current_version + warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell." + end end diff --git a/config/routes/project.rb b/config/routes/project.rb index 19e18c733b1..0d0a8dff25e 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -87,13 +87,8 @@ constraints(ProjectUrlConstrainer.new) do resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] - resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do + resources :merge_requests, concerns: :awardable, except: [:new, :create], constraints: { id: /\d+/ } do member do - get :commits - get :diffs - get :conflicts - get :conflict_for_path - get :pipelines get :commit_change_content post :merge post :cancel_merge_when_pipeline_succeeds @@ -101,18 +96,32 @@ constraints(ProjectUrlConstrainer.new) do get :ci_environments_status post :toggle_subscription post :remove_wip - get :diff_for_path - post :resolve_conflicts post :assign_related_issues + + scope constraints: { format: nil }, action: :show do + get :commits, defaults: { tab: 'commits' } + get :pipelines, defaults: { tab: 'pipelines' } + get :diffs, defaults: { tab: 'diffs' } + end + + scope constraints: { format: 'json' }, as: :json do + get :commits + get :pipelines + get :diffs, to: 'merge_requests/diffs#show' + end + + get :diff_for_path, controller: 'merge_requests/diffs' + + scope controller: 'merge_requests/conflicts' do + get :conflicts, action: :show + get :conflict_for_path + post :resolve_conflicts + end end collection do - get :branch_from - get :branch_to - get :update_branches get :diff_for_path post :bulk_update - get :new_diffs, path: 'new/diffs' end resources :discussions, only: [], constraints: { id: /\h{40}/ } do @@ -123,6 +132,29 @@ constraints(ProjectUrlConstrainer.new) do end end + controller 'merge_requests/creations', path: 'merge_requests' do + post '', action: :create, as: nil + + scope path: 'new', as: :new_merge_request do + get '', action: :new + + scope constraints: { format: nil }, action: :new do + get :diffs, defaults: { tab: 'diffs' } + get :pipelines, defaults: { tab: 'pipelines' } + end + + scope constraints: { format: 'json' }, as: :json do + get :diffs + get :pipelines + end + + get :diff_for_path + get :update_branches + get :branch_from + get :branch_to + end + end + resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :edit, :update, :destroy] do member do diff --git a/config/webpack.config.js b/config/webpack.config.js index 2e8c94655c1..90ef6a5448b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -163,6 +163,7 @@ var config = { 'issue_show', 'job_details', 'merge_conflicts', + 'monitoring', 'notebook_viewer', 'pdf_viewer', 'pipelines', 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/post_migrate/20170525140254_rename_all_reserved_paths_again.rb b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb new file mode 100644 index 00000000000..9441b236c8d --- /dev/null +++ b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb @@ -0,0 +1,113 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameAllReservedPathsAgain < ActiveRecord::Migration + include Gitlab::Database::RenameReservedPathsMigration::V1 + + DOWNTIME = false + + disable_ddl_transaction! + + TOP_LEVEL_ROUTES = %w[ + - + .well-known + abuse_reports + admin + all + api + assets + autocomplete + ci + dashboard + explore + files + groups + health_check + help + hooks + import + invites + issues + jwt + koding + member + merge_requests + new + notes + notification_settings + oauth + profile + projects + public + repository + robots.txt + s + search + sent_notifications + services + snippets + teams + u + unicorn_test + unsubscribes + uploads + users + ].freeze + + PROJECT_WILDCARD_ROUTES = %w[ + badges + blame + blob + builds + commits + create + create_dir + edit + environments/folders + files + find_file + gitlab-lfs/objects + info/lfs/objects + new + preview + raw + refs + tree + update + wikis + ].freeze + + GROUP_ROUTES = %w[ + activity + analytics + audit_events + avatar + edit + group_members + hooks + issues + labels + ldap + ldap_group_links + merge_requests + milestones + notification_setting + pipeline_quota + projects + subgroups + ].freeze + + def up + disable_statement_timeout + + TOP_LEVEL_ROUTES.each { |route| rename_root_paths(route) } + PROJECT_WILDCARD_ROUTES.each { |route| rename_wildcard_paths(route) } + GROUP_ROUTES.each { |route| rename_child_paths(route) } + end + + def down + disable_statement_timeout + + revert_renames + end +end diff --git a/db/schema.rb b/db/schema.rb index 028556bdccf..8c7440ee610 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170621102400) do +ActiveRecord::Schema.define(version: 20170622162730) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -770,6 +770,7 @@ ActiveRecord::Schema.define(version: 20170621102400) 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 diff --git a/doc/README.md b/doc/README.md index ab8ea192a26..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._ ---- @@ -125,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/api/features.md b/doc/api/features.md index 89b8d3ac948..558869255cc 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -58,6 +58,10 @@ POST /features/:name | --------- | ---- | -------- | ----------- | | `name` | string | yes | Name of the feature to create or update | | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | +| `feature_group` | string | no | A Feature group name | +| `user` | string | no | A GitLab username | + +Note that `feature_group` and `user` are mutually exclusive. ```bash curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 4ad6071a0ed..8133251dffe 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -29,22 +29,30 @@ Example response: { "id": 1, "path": "user1", - "kind": "user" + "kind": "user", + "full_path": "user1" }, { "id": 2, "path": "group1", - "kind": "group" + "kind": "group", + "full_path": "group1", + "parent_id": "null", + "members_count_with_descendants": 2 }, { "id": 3, "path": "bar", "kind": "group", "full_path": "foo/bar", + "parent_id": "9", + "members_count_with_descendants": 5 } ] ``` +**Note**: `members_count_with_descendants` are presented only for group masters/owners. + ## Search for namespace Get all namespaces that match a string in their name or path. @@ -72,6 +80,8 @@ Example response: "path": "twitter", "kind": "group", "full_path": "twitter", + "parent_id": "null", + "members_count_with_descendants": 2 } ] ``` 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/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index befaa06e918..cf25a8b618f 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -34,9 +34,9 @@ instructions to [generate an SSH key](../../ssh/README.md). Do not add a passphrase to the SSH key, or the `before_script` will prompt for it. Then, create a new **Secret Variable** in your project settings on GitLab -following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` -and in the **Value** field paste the content of your _private_ key that you -created earlier. +following **Settings > Pipelines** and look for the "Secret Variables" section. +As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the +content of your _private_ key that you created earlier. It is also good practice to check the server's own public key to make sure you are not being targeted by a man-in-the-middle attack. To do this, add another diff --git a/doc/development/README.md b/doc/development/README.md index 9496a87d84d..a2a07c37ced 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -54,6 +54,7 @@ - [Polymorphic Associations](polymorphic_associations.md) - [Single Table Inheritance](single_table_inheritance.md) - [Background Migrations](background_migrations.md) +- [Storing SHA1 Hashes As Binary](sha1_as_binary.md) ## i18n 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/policies.md b/doc/development/policies.md new file mode 100644 index 00000000000..62141356f59 --- /dev/null +++ b/doc/development/policies.md @@ -0,0 +1,116 @@ +# `DeclarativePolicy` framework + +The DeclarativePolicy framework is designed to assist in performance of policy checks, and to enable ease of extension for EE. The DSL code in `app/policies` is what `Ability.allowed?` uses to check whether a particular action is allowed on a subject. + +The policy used is based on the subject's class name - so `Ability.allowed?(user, :some_ability, project)` will create a `ProjectPolicy` and check permissions on that. + +## Managing Permission Rules + +Permissions are broken into two parts: `conditions` and `rules`. Conditions are boolean expressions that can access the database and the environment, while rules are statically configured combinations of expressions and other rules that enable or prevent certain abilities. For an ability to be allowed, it must be enabled by at least one rule, and not prevented by any. + + +### Conditions + +Conditions are defined by the `condition` method, and are given a name and a block. The block will be executed in the context of the policy object - so it can access `@user` and `@subject`, as well as call any methods defined on the policy. Note that `@user` may be nil (in the anonymous case), but `@subject` is guaranteed to be a real instance of the subject class. + +``` ruby +class FooPolicy < BasePolicy + condition(:is_public) do + # @subject guaranteed to be an instance of Foo + @subject.public? + end + + # instance methods can be called from the condition as well + condition(:thing) { check_thing } + + def check_thing + # ... + end +end +``` + +When you define a condition, a predicate method is defined on the policy to check whether that condition passes - so in the above example, an instance of `FooPolicy` will also respond to `#is_public?` and `#thing?`. + +Conditions are cached according to their scope. Scope and ordering will be covered later. + +### Rules + +A `rule` is a logical combination of conditions and other rules, that are configured to enable or prevent certain abilities. It is important to note that the rule configuration is static - a rule's logic cannot touch the database or know about `@user` or `@subject`. This allows us to cache only at the condition level. Rules are specified through the `rule` method, which takes a block of DSL configuration, and returns an object that responds to `#enable` or `#prevent`: + +``` ruby +class FooPolicy < BasePolicy + # ... + + rule { is_public }.enable :read + rule { thing }.prevent :read + + # equivalently, + rule { is_public }.policy do + enable :read + end + + rule { ~thing }.policy do + prevent :read + end +end +``` + +Within the rule DSL, you can use: + +* A regular word mentions a condition by name - a rule that is in effect when that condition is truthy. +* `~` indicates negation +* `&` and `|` are logical combinations, also available as `all?(...)` and `any?(...)` +* `can?(:other_ability)` delegates to the rules that apply to `:other_ability`. Note that this is distinct from the instance method `can?`, which can check dynamically - this only configures a delegation to another ability. + +## Scores, Order, Performance + +To see how the rules get evaluated into a judgment, it is useful in a console to use `policy.debug(:some_ability)`. This will print the rules in the order they are evaluated. + +When a policy is asked whether a particular ability is allowed (`policy.allowed?(:some_ability)`), it does not necessarily have to compute all the conditions on the policy. First, only the rules relevant to that particular ability are selected. Then, the execution model takes advantage of short-circuiting, and attempts to sort rules based on a heuristic of how expensive they will be to calculate. The sorting is dynamic and cache-aware, so that previously calculated conditions will be considered first, before computing other conditions. + +## Scope + +Sometimes, a condition will only use data from `@user` or only from `@subject`. In this case, we want to change the scope of the caching, so that we don't recalculate conditions unnecessarily. For example, given: + +``` ruby +class FooPolicy < BasePolicy + condition(:expensive_condition) { @subject.expensive_query? } + + rule { expensive_condition }.enable :some_ability +end +``` + +Naively, if we call `Ability.can?(user1, :some_ability, foo)` and `Ability.can?(user2, :some_ability, foo)`, we would have to calculate the condition twice - since they are for different users. But if we use the `scope: :subject` option: + +``` ruby + condition(:expensive_condition, scope: :subject) { @subject.expensive_query? } +``` + +then the result of the condition will be cached globally only based on the subject - so it will not be calculated repeatedly for different users. Similarly, `scope: :user` will cache only based on the user. + +**DANGER**: If you use a `:scope` option when the condition actually uses data from +both user and subject (including a simple anonymous check!) your result will be cached at too global of a scope and will result in cache bugs. + +Sometimes we are checking permissions for a lot of users for one subject, or a lot of subjects for one user. In this case, we want to set a *preferred scope* - i.e. tell the system that we prefer rules that can be cached on the repeated parameter. For example, in `Ability.users_that_can_read_project`: + +``` ruby +def users_that_can_read_project(users, project) + DeclarativePolicy.subject_scope do + users.select { |u| allowed?(u, :read_project, project) } + end +end +``` + +This will, for example, prefer checking `project.public?` to checking `user.admin?`. + +## Delegation + +Delegation is the inclusion of rules from another policy, on a different subject. For example, + +``` ruby +class FooPolicy < BasePolicy + delegate { @subject.project } +end +``` + +will include all rules from `ProjectPolicy`. The delegated conditions will be evaluated with the correct delegated subject, and will be sorted along with the regular rules in the policy. Note that only the relevant rules for a particular ability will actually be considered. diff --git a/doc/development/sha1_as_binary.md b/doc/development/sha1_as_binary.md new file mode 100644 index 00000000000..3151cc29bbc --- /dev/null +++ b/doc/development/sha1_as_binary.md @@ -0,0 +1,36 @@ +# Storing SHA1 Hashes As Binary + +Storing SHA1 hashes as strings is not very space efficient. A SHA1 as a string +requires at least 40 bytes, an additional byte to store the encoding, and +perhaps more space depending on the internals of PostgreSQL and MySQL. + +On the other hand, if one were to store a SHA1 as binary one would only need 20 +bytes for the actual SHA1, and 1 or 4 bytes of additional space (again depending +on database internals). This means that in the best case scenario we can reduce +the space usage by 50%. + +To make this easier to work with you can include the concern `ShaAttribute` into +a model and define a SHA attribute using the `sha_attribute` class method. For +example: + +```ruby +class Commit < ActiveRecord::Base + include ShaAttribute + + sha_attribute :sha +end +``` + +This allows you to use the value of the `sha` attribute as if it were a string, +while storing it as binary. This means that you can do something like this, +without having to worry about converting data to the right binary format: + +```ruby +commit = Commit.find_by(sha: '88c60307bd1f215095834f09a1a5cb18701ac8ad') +commit.sha = '971604de4cfa324d91c41650fabc129420c8d1cc' +commit.save +``` + +There is however one requirement: the column used to store the SHA has _must_ be +a binary type. For Rails this means you need to use the `:binary` type instead +of `:text` or `:string`. diff --git a/doc/install/installation.md b/doc/install/installation.md index 84af6432889..992ff162efb 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -294,9 +294,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-2-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-3-stable gitlab -**Note:** You can change `9-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `9-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It 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/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 9488ce1ef30..f28c034e74c 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -14,6 +14,9 @@ accepted method of authentication when you have Once you have your token, [pass it to the API][usage] using either the `private_token` parameter or the `PRIVATE-TOKEN` header. +The expiration of personal access tokens happens on the date you define, +at midnight UTC. + ## Creating a personal access token You can create as many personal access tokens as you like from your GitLab diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differindex b636cb294b8..cf7f519f783 100644 --- a/doc/user/project/img/issue_board.png +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png Binary files differindex cdfc466d23f..973d9f7cde4 100644 --- a/doc/user/project/img/issue_board_add_list.png +++ b/doc/user/project/img/issue_board_add_list.png diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png Binary files differindex 5318e6ea4a9..127b9b08cc7 100644 --- a/doc/user/project/img/issue_board_welcome_message.png +++ b/doc/user/project/img/issue_board_welcome_message.png diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png Binary files differindex 33049dce74f..bedaf724a15 100644 --- a/doc/user/project/img/issue_boards_add_issues_modal.png +++ b/doc/user/project/img/issue_boards_add_issues_modal.png 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/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/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 71c69a4fdea..3c06b188f3e 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -27,7 +27,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I see prefilled new Merge Request page' do expect(page).to have_selector('.merge-request-form') - expect(current_path).to eq new_namespace_project_merge_request_path(@project.namespace, @project) + expect(current_path).to eq namespace_project_new_merge_request_path(@project.namespace, @project) expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s expect(find("input#merge_request_source_branch").value).to eq "fix" expect(find("input#merge_request_target_branch").value).to eq "master" 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/merge_requests.rb b/features/steps/project/merge_requests.rb index 69f5d0f8410..dceeed5aafe 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -65,7 +65,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should not see "master" branch' do - expect(find('.merge-request-info')).not_to have_content "master" + expect(find('.issuable-info')).not_to have_content "master" end step 'I should see "feature_conflict" branch' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 80aa3a047a0..02434319a08 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -266,12 +266,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the new merge request page' do - expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project)) + expect(current_path).to eq(namespace_project_new_merge_request_path(@project.namespace, @project)) end step "I am redirected to the fork's new merge request page" do fork = @user.fork_of(@project) - expect(current_path).to eq(new_namespace_project_merge_request_path(fork.namespace, fork)) + expect(current_path).to eq(namespace_project_new_merge_request_path(fork.namespace, fork)) end step 'I am redirected to the root directory' do @@ -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/diff_note.rb b/features/steps/shared/diff_note.rb index 36fc315599e..2c59ec5bb06 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -232,7 +232,7 @@ module SharedDiffNote end def click_parallel_diff_line(code, line_type) - find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover' + find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' end 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/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/entities.rb b/lib/api/entities.rb index aa91451c9f4..cef5a0abe12 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -444,7 +444,15 @@ module API end class Namespace < Grape::Entity - expose :id, :name, :path, :kind, :full_path + expose :id, :name, :path, :kind, :full_path, :parent_id + + expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _| + namespace.users_with_descendants.count + end + + def expose_members_count_with_descendants?(namespace, opts) + namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace) + end end class MemberAccess < Grape::Entity diff --git a/lib/api/features.rb b/lib/api/features.rb index cff0ba2ddff..21745916463 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -2,6 +2,29 @@ module API class Features < Grape::API before { authenticated_as_admin! } + helpers do + def gate_value(params) + case params[:value] + when 'true' + true + when '0', 'false' + false + else + params[:value].to_i + end + end + + def gate_target(params) + if params[:feature_group] + Feature.group(params[:feature_group]) + elsif params[:user] + User.find_by_username(params[:user]) + else + gate_value(params) + end + end + end + resource :features do desc 'Get a list of all features' do success Entities::Feature @@ -17,16 +40,22 @@ module API end params do requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' + optional :feature_group, type: String, desc: 'A Feature group name' + optional :user, type: String, desc: 'A GitLab username' + mutually_exclusive :feature_group, :user end post ':name' do feature = Feature.get(params[:name]) + target = gate_target(params) + value = gate_value(params) - if %w(0 false).include?(params[:value]) - feature.disable - elsif params[:value] == 'true' - feature.enable + case value + when true + feature.enable(target) + when false + feature.disable(target) else - feature.enable_percentage_of_time(params[:value].to_i) + feature.enable_percentage_of_time(value) end present feature, with: Entities::Feature, current_user: current_user diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 1369b021ea4..f8645e364ce 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -46,7 +46,8 @@ module API yield if block_given? - forbidden!('Project has been deleted!') unless job.project + project = job.project + forbidden!('Project has been deleted!') if project.nil? || project.pending_delete? forbidden!('Job has been erased!') if job.erased? end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 479ee16a611..f1c79970ba4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -132,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/namespaces.rb b/lib/api/namespaces.rb index 30761cb9b55..f1eaff6b0eb 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -17,7 +17,7 @@ module API namespaces = namespaces.search(params[:search]) if params[:search].present? - present paginate(namespaces), with: Entities::Namespace + present paginate(namespaces), with: Entities::Namespace, current_user: current_user end end end 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 c5df45b7902..d0bd64b2972 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -1,3 +1,5 @@ +require_dependency 'declarative_policy' + module API # Projects API class Projects < Grape::API @@ -396,7 +398,7 @@ module API use :pagination end get ':id/users' do - users = user_project.team.users + users = DeclarativePolicy.subject_scope { user_project.team.users } users = users.search(params[:search]) if params[:search].present? present paginate(users), with: Entities::UserBasic diff --git a/lib/api/users.rb b/lib/api/users.rb index 18ce58299e7..bad4d76b428 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -105,7 +105,7 @@ 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 @@ -163,7 +163,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) @@ -241,9 +243,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 @@ -281,8 +283,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 @@ -494,9 +495,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 @@ -512,8 +513,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' 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/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 5109dc9670f..a40b6ab6c9f 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -28,7 +28,8 @@ module Ci yield if block_given? - forbidden!('Project has been deleted!') unless build.project + project = build.project + forbidden!('Project has been deleted!') if project.nil? || project.pending_delete? forbidden!('Build has been erased!') if build.erased? end diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index 6063d6f45e8..872e418c788 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -3,7 +3,7 @@ module Ci module DailyInterval def grouped_count(query) query - .group("DATE(#{Ci::Build.table_name}.created_at)") + .group("DATE(#{Ci::Pipeline.table_name}.created_at)") .count(:created_at) .transform_keys { |date| date.strftime(@format) } end @@ -17,12 +17,12 @@ module Ci def grouped_count(query) if Gitlab::Database.postgresql? query - .group("to_char(#{Ci::Build.table_name}.created_at, '01 Month YYYY')") + .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')") + .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/declarative_policy.rb b/lib/declarative_policy.rb new file mode 100644 index 00000000000..d9959bc1aff --- /dev/null +++ b/lib/declarative_policy.rb @@ -0,0 +1,58 @@ +require_dependency 'declarative_policy/cache' +require_dependency 'declarative_policy/condition' +require_dependency 'declarative_policy/dsl' +require_dependency 'declarative_policy/preferred_scope' +require_dependency 'declarative_policy/rule' +require_dependency 'declarative_policy/runner' +require_dependency 'declarative_policy/step' + +require_dependency 'declarative_policy/base' + +module DeclarativePolicy + class << self + def policy_for(user, subject, opts = {}) + cache = opts[:cache] || {} + key = Cache.policy_key(user, subject) + + cache[key] ||= class_for(subject).new(user, subject, opts) + end + + def class_for(subject) + return GlobalPolicy if subject == :global + return NilPolicy if subject.nil? + + subject = find_delegate(subject) + + subject.class.ancestors.each do |klass| + next unless klass.name + + begin + policy_class = "#{klass.name}Policy".constantize + + # NOTE: the < operator here tests whether policy_class + # inherits from Base. We can't use #is_a? because that + # tests for *instances*, not *subclasses*. + return policy_class if policy_class < Base + rescue NameError + nil + end + end + + raise "no policy for #{subject.class.name}" + end + + private + + def find_delegate(subject) + seen = Set.new + + while subject.respond_to?(:declarative_policy_delegate) + raise ArgumentError, "circular delegations" if seen.include?(subject.object_id) + seen << subject.object_id + subject = subject.declarative_policy_delegate + end + + subject + end + end +end diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb new file mode 100644 index 00000000000..df94cafb6a1 --- /dev/null +++ b/lib/declarative_policy/base.rb @@ -0,0 +1,329 @@ +module DeclarativePolicy + class Base + # A map of ability => list of rules together with :enable + # or :prevent actions. Used to look up which rules apply to + # a given ability. See Base.ability_map + class AbilityMap + attr_reader :map + def initialize(map = {}) + @map = map + end + + # This merge behavior is different than regular hashes - if both + # share a key, the values at that key are concatenated, rather than + # overridden. + def merge(other) + conflict_proc = proc { |key, my_val, other_val| my_val + other_val } + AbilityMap.new(@map.merge(other.map, &conflict_proc)) + end + + def actions(key) + @map[key] ||= [] + end + + def enable(key, rule) + actions(key) << [:enable, rule] + end + + def prevent(key, rule) + actions(key) << [:prevent, rule] + end + end + + class << self + # The `own_ability_map` vs `ability_map` distinction is used so that + # the data structure is properly inherited - with subclasses recursively + # merging their parent class. + # + # This pattern is also used for conditions, global_actions, and delegations. + def ability_map + if self == Base + own_ability_map + else + superclass.ability_map.merge(own_ability_map) + end + end + + def own_ability_map + @own_ability_map ||= AbilityMap.new + end + + # an inheritable map of conditions, by name + def conditions + if self == Base + own_conditions + else + superclass.conditions.merge(own_conditions) + end + end + + def own_conditions + @own_conditions ||= {} + end + + # a list of global actions, generated by `prevent_all`. these aren't + # stored in `ability_map` because they aren't indexed by a particular + # ability. + def global_actions + if self == Base + own_global_actions + else + superclass.global_actions + own_global_actions + end + end + + def own_global_actions + @own_global_actions ||= [] + end + + # an inheritable map of delegations, indexed by name (which may be + # autogenerated) + def delegations + if self == Base + own_delegations + else + superclass.delegations.merge(own_delegations) + end + end + + def own_delegations + @own_delegations ||= {} + end + + # all the [rule, action] pairs that apply to a particular ability. + # we combine the specific ones looked up in ability_map with the global + # ones. + def configuration_for(ability) + ability_map.actions(ability) + global_actions + end + + ### declaration methods ### + + def delegate(name = nil, &delegation_block) + if name.nil? + @delegate_name_counter ||= 0 + @delegate_name_counter += 1 + name = :"anonymous_#{@delegate_name_counter}" + end + + name = name.to_sym + + if delegation_block.nil? + delegation_block = proc { @subject.__send__(name) } + end + + own_delegations[name] = delegation_block + end + + # Declares a rule, constructed using RuleDsl, and returns + # a PolicyDsl which is used for registering the rule with + # this class. PolicyDsl will call back into Base.enable_when, + # Base.prevent_when, and Base.prevent_all_when. + def rule(&b) + rule = RuleDsl.new(self).instance_eval(&b) + PolicyDsl.new(self, rule) + end + + # A hash in which to store calls to `desc` and `with_scope`, etc. + def last_options + @last_options ||= {}.with_indifferent_access + end + + # retrieve and zero out the previously set options (used in .condition) + def last_options! + last_options.tap { @last_options = nil } + end + + # Declare a description for the following condition. Currently unused, + # but opens the potential for explaining to users why they were or were + # not able to do something. + def desc(description) + last_options[:description] = description + end + + def with_options(opts = {}) + last_options.merge!(opts) + end + + def with_scope(scope) + with_options scope: scope + end + + def with_score(score) + with_options score: score + end + + # Declares a condition. It gets stored in `own_conditions`, and generates + # a query method based on the condition's name. + def condition(name, opts = {}, &value) + name = name.to_sym + + opts = last_options!.merge(opts) + opts[:context_key] ||= self.name + + condition = Condition.new(name, opts, &value) + + self.own_conditions[name] = condition + + define_method(:"#{name}?") { condition(name).pass? } + end + + # These next three methods are mainly called from PolicyDsl, + # and are responsible for "inverting" the relationship between + # an ability and a rule. We store in `ability_map` a map of + # abilities to rules that affect them, together with a + # symbol indicating :prevent or :enable. + def enable_when(abilities, rule) + abilities.each { |a| own_ability_map.enable(a, rule) } + end + + def prevent_when(abilities, rule) + abilities.each { |a| own_ability_map.prevent(a, rule) } + end + + # we store global prevents (from `prevent_all`) separately, + # so that they can be combined into every decision made. + def prevent_all_when(rule) + own_global_actions << [:prevent, rule] + end + end + + # A policy object contains a specific user and subject on which + # to compute abilities. For this reason it's sometimes called + # "context" within the framework. + # + # It also stores a reference to the cache, so it can be used + # to cache computations by e.g. ManifestCondition. + attr_reader :user, :subject, :cache + def initialize(user, subject, opts = {}) + @user = user + @subject = subject + @cache = opts[:cache] || {} + end + + # helper for checking abilities on this and other subjects + # for the current user. + def can?(ability, new_subject = :_self) + return allowed?(ability) if new_subject == :_self + + policy_for(new_subject).allowed?(ability) + end + + # This is the main entry point for permission checks. It constructs + # or looks up a Runner for the given ability and asks it if it passes. + def allowed?(*abilities) + abilities.all? { |a| runner(a).pass? } + end + + # The inverse of #allowed?, used mainly in specs. + def disallowed?(*abilities) + abilities.all? { |a| !runner(a).pass? } + end + + # computes the given ability and prints a helpful debugging output + # showing which + def debug(ability, *a) + runner(ability).debug(*a) + end + + desc "Unknown user" + condition(:anonymous, scope: :user, score: 0) { @user.nil? } + + desc "By default" + condition(:default, scope: :global, score: 0) { true } + + def repr + subject_repr = + if @subject.respond_to?(:id) + "#{@subject.class.name}/#{@subject.id}" + else + @subject.inspect + end + + user_repr = + if @user + @user.to_reference + else + "<anonymous>" + end + + "(#{user_repr} : #{subject_repr})" + end + + def inspect + "#<#{self.class.name} #{repr}>" + end + + # returns a Runner for the given ability, capable of computing whether + # the ability is allowed. Runners are cached on the policy (which itself + # is cached on @cache), and caches its result. This is how we perform caching + # at the ability level. + def runner(ability) + ability = ability.to_sym + @runners ||= {} + @runners[ability] ||= + begin + delegated_runners = delegated_policies.values.compact.map { |p| p.runner(ability) } + own_runner = Runner.new(own_steps(ability)) + delegated_runners.inject(own_runner, &:merge_runner) + end + end + + # Helpers for caching. Used by ManifestCondition in performing condition + # computation. + # + # NOTE we can't use ||= here because the value might be the + # boolean `false` + def cache(key, &b) + return @cache[key] if cached?(key) + @cache[key] = yield + end + + def cached?(key) + !@cache[key].nil? + end + + # returns a ManifestCondition capable of computing itself. The computation + # will use our own @cache. + def condition(name) + name = name.to_sym + @_conditions ||= {} + @_conditions[name] ||= + begin + raise "invalid condition #{name}" unless self.class.conditions.key?(name) + ManifestCondition.new(self.class.conditions[name], self) + end + end + + # used in specs - returns true if there is no possible way for any action + # to be allowed, determined only by the global :prevent_all rules. + def banned? + global_steps = self.class.global_actions.map { |(action, rule)| Step.new(self, rule, action) } + !Runner.new(global_steps).pass? + end + + # A list of other policies that we've delegated to (see `Base.delegate`) + def delegated_policies + @delegated_policies ||= self.class.delegations.transform_values do |block| + new_subject = instance_eval(&block) + + # never delegate to nil, as that would immediately prevent_all + next if new_subject.nil? + + policy_for(new_subject) + end + end + + def policy_for(other_subject) + DeclarativePolicy.policy_for(@user, other_subject, cache: @cache) + end + + protected + + # constructs steps that come from this policy and not from any delegations + def own_steps(ability) + rules = self.class.configuration_for(ability) + rules.map { |(action, rule)| Step.new(self, rule, action) } + end + end +end diff --git a/lib/declarative_policy/cache.rb b/lib/declarative_policy/cache.rb new file mode 100644 index 00000000000..b8cc60074c7 --- /dev/null +++ b/lib/declarative_policy/cache.rb @@ -0,0 +1,32 @@ +module DeclarativePolicy + module Cache + class << self + def user_key(user) + return '<anonymous>' if user.nil? + id_for(user) + end + + def policy_key(user, subject) + u = user_key(user) + s = subject_key(subject) + "/dp/policy/#{u}/#{s}" + end + + def subject_key(subject) + return '<nil>' if subject.nil? + return subject.inspect if subject.is_a?(Symbol) + "#{subject.class.name}:#{id_for(subject)}" + end + + private + + def id_for(obj) + if obj.respond_to?(:id) && obj.id + obj.id.to_s + else + "##{obj.object_id}" + end + end + end + end +end diff --git a/lib/declarative_policy/condition.rb b/lib/declarative_policy/condition.rb new file mode 100644 index 00000000000..9d7cf6b9726 --- /dev/null +++ b/lib/declarative_policy/condition.rb @@ -0,0 +1,102 @@ +module DeclarativePolicy + # A Condition is the data structure that is created by the + # `condition` declaration on DeclarativePolicy::Base. It is + # more or less just a struct of the data passed to that + # declaration. It holds on to the block to be instance_eval'd + # on a context (instance of Base) later, via #compute. + class Condition + attr_reader :name, :description, :scope + attr_reader :manual_score + attr_reader :context_key + def initialize(name, opts = {}, &compute) + @name = name + @compute = compute + @scope = opts.fetch(:scope, :normal) + @description = opts.delete(:description) + @context_key = opts[:context_key] + @manual_score = opts.fetch(:score, nil) + end + + def compute(context) + !!context.instance_eval(&@compute) + end + + def key + "#{@context_key}/#{@name}" + end + end + + # In contrast to a Condition, a ManifestCondition contains + # a Condition and a context object, and is capable of calculating + # a result itself. This is the return value of Base#condition. + class ManifestCondition + def initialize(condition, context) + @condition = condition + @context = context + end + + # The main entry point - does this condition pass? We reach into + # the context's cache here so that we can share in the global + # cache (often RequestStore or similar). + def pass? + @context.cache(cache_key) { @condition.compute(@context) } + end + + # Whether we've already computed this condition. + def cached? + @context.cached?(cache_key) + end + + # This is used to score Rule::Condition. See Rule::Condition#score + # and Runner#steps_by_score for how scores are used. + # + # The number here is intended to represent, abstractly, how + # expensive it would be to calculate this condition. + # + # See #cache_key for info about @condition.scope. + def score + # If we've been cached, no computation is necessary. + return 0 if cached? + + # Use the override from condition(score: ...) if present + return @condition.manual_score if @condition.manual_score + + # Global scope rules are cheap due to max cache sharing + return 2 if @condition.scope == :global + + # "Normal" rules can't share caches with any other policies + return 16 if @condition.scope == :normal + + # otherwise, we're :user or :subject scope, so it's 4 if + # the caller has declared a preference + return 4 if @condition.scope == DeclarativePolicy.preferred_scope + + # and 8 for all other :user or :subject scope conditions. + 8 + end + + private + + # This method controls the caching for the condition. This is where + # the condition(scope: ...) option comes into play. Notice that + # depending on the scope, we may cache only by the user or only by + # the subject, resulting in sharing across different policy objects. + def cache_key + case @condition.scope + when :normal then "/dp/condition/#{@condition.key}/#{user_key},#{subject_key}" + when :user then "/dp/condition/#{@condition.key}/#{user_key}" + when :subject then "/dp/condition/#{@condition.key}/#{subject_key}" + when :global then "/dp/condition/#{@condition.key}" + else raise 'invalid scope' + end + end + + def user_key + Cache.user_key(@context.user) + end + + def subject_key + Cache.subject_key(@context.subject) + end + end +end diff --git a/lib/declarative_policy/dsl.rb b/lib/declarative_policy/dsl.rb new file mode 100644 index 00000000000..b26807a7622 --- /dev/null +++ b/lib/declarative_policy/dsl.rb @@ -0,0 +1,103 @@ +module DeclarativePolicy + # The DSL evaluation context inside rule { ... } blocks. + # Responsible for creating and combining Rule objects. + # + # See Base.rule + class RuleDsl + def initialize(context_class) + @context_class = context_class + end + + def can?(ability) + Rule::Ability.new(ability) + end + + def all?(*rules) + Rule::And.make(rules) + end + + def any?(*rules) + Rule::Or.make(rules) + end + + def none?(*rules) + ~Rule::Or.new(rules) + end + + def cond(condition) + Rule::Condition.new(condition) + end + + def delegate(delegate_name, condition) + Rule::DelegatedCondition.new(delegate_name, condition) + end + + def method_missing(m, *a, &b) + return super unless a.size == 0 && !block_given? + + if @context_class.delegations.key?(m) + DelegateDsl.new(self, m) + else + cond(m.to_sym) + end + end + end + + # Used when the name of a delegate is mentioned in + # the rule DSL. + class DelegateDsl + def initialize(rule_dsl, delegate_name) + @rule_dsl = rule_dsl + @delegate_name = delegate_name + end + + def method_missing(m, *a, &b) + return super unless a.size == 0 && !block_given? + + @rule_dsl.delegate(@delegate_name, m) + end + end + + # The return value of a rule { ... } declaration. + # Can call back to register rules with the containing + # Policy class (context_class here). See Base.rule + # + # Note that the #policy method just performs an #instance_eval, + # which is useful for multiple #enable or #prevent callse. + # + # Also provides a #method_missing proxy to the context + # class's class methods, so that helper methods can be + # defined and used in a #policy { ... } block. + class PolicyDsl + def initialize(context_class, rule) + @context_class = context_class + @rule = rule + end + + def policy(&b) + instance_eval(&b) + end + + def enable(*abilities) + @context_class.enable_when(abilities, @rule) + end + + def prevent(*abilities) + @context_class.prevent_when(abilities, @rule) + end + + def prevent_all + @context_class.prevent_all_when(@rule) + end + + def method_missing(m, *a, &b) + return super unless @context_class.respond_to?(m) + + @context_class.__send__(m, *a, &b) + end + + def respond_to_missing?(m) + @context_class.respond_to?(m) || super + end + end +end diff --git a/lib/declarative_policy/preferred_scope.rb b/lib/declarative_policy/preferred_scope.rb new file mode 100644 index 00000000000..b0754098149 --- /dev/null +++ b/lib/declarative_policy/preferred_scope.rb @@ -0,0 +1,28 @@ +module DeclarativePolicy + PREFERRED_SCOPE_KEY = :"DeclarativePolicy.preferred_scope" + + class << self + def with_preferred_scope(scope, &b) + Thread.current[PREFERRED_SCOPE_KEY], old_scope = scope, Thread.current[PREFERRED_SCOPE_KEY] + yield + ensure + Thread.current[PREFERRED_SCOPE_KEY] = old_scope + end + + def preferred_scope + Thread.current[PREFERRED_SCOPE_KEY] + end + + def user_scope(&b) + with_preferred_scope(:user, &b) + end + + def subject_scope(&b) + with_preferred_scope(:subject, &b) + end + + def preferred_scope=(scope) + Thread.current[PREFERRED_SCOPE_KEY] = scope + end + end +end diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb new file mode 100644 index 00000000000..bfcec241489 --- /dev/null +++ b/lib/declarative_policy/rule.rb @@ -0,0 +1,301 @@ +module DeclarativePolicy + module Rule + # A Rule is the object that results from the `rule` declaration, + # usually built using the DSL in `RuleDsl`. It is a basic logical + # combination of building blocks, and is capable of deciding, + # given a context (instance of DeclarativePolicy::Base) whether it + # passes or not. Note that this decision doesn't by itself know + # how that affects the actual ability decision - for that, a + # `Step` is used. + class Base + def self.make(*a) + new(*a).simplify + end + + # true or false whether this rule passes. + # `context` is a policy - an instance of + # DeclarativePolicy::Base. + def pass?(context) + raise 'abstract' + end + + # same as #pass? except refuses to do any I/O, + # returning nil if the result is not yet cached. + # used for accurately scoring And/Or + def cached_pass?(context) + raise 'abstract' + end + + # abstractly, how long would it take to compute + # this rule? lower-scored rules are tried first. + def score(context) + raise 'abstract' + end + + # unwrap double negatives and nested and/or + def simplify + self + end + + # convenience combination methods + def or(other) + Or.make([self, other]) + end + + def and(other) + And.make([self, other]) + end + + def negate + Not.make(self) + end + + alias_method :|, :or + alias_method :&, :and + alias_method :~@, :negate + + def inspect + "#<Rule #{repr}>" + end + end + + # A rule that checks a condition. This is the + # type of rule that results from a basic bareword + # in the rule dsl (see RuleDsl#method_missing). + class Condition < Base + def initialize(name) + @name = name + end + + # we delegate scoring to the condition. See + # ManifestCondition#score. + def score(context) + context.condition(@name).score + end + + # Let the ManifestCondition from the context + # decide whether we pass. + def pass?(context) + context.condition(@name).pass? + end + + # returns nil unless it's already cached + def cached_pass?(context) + condition = context.condition(@name) + return nil unless condition.cached? + condition.pass? + end + + def description(context) + context.class.conditions[@name].description + end + + def repr + @name.to_s + end + end + + # A rule constructed from DelegateDsl - using a condition from a + # delegated policy. + class DelegatedCondition < Base + # Internal use only - this is rescued each time it's raised. + MissingDelegate = Class.new(StandardError) + + def initialize(delegate_name, name) + @delegate_name = delegate_name + @name = name + end + + def delegated_context(context) + policy = context.delegated_policies[@delegate_name] + raise MissingDelegate if policy.nil? + policy + end + + def score(context) + delegated_context(context).condition(@name).score + rescue MissingDelegate + 0 + end + + def cached_pass?(context) + condition = delegated_context(context).condition(@name) + return nil unless condition.cached? + condition.pass? + rescue MissingDelegate + false + end + + def pass?(context) + delegated_context(context).condition(@name).pass? + rescue MissingDelegate + false + end + + def repr + "#{@delegate_name}.#{@name}" + end + end + + # A rule constructed from RuleDsl#can?. Computes a different ability + # on the same subject. + class Ability < Base + attr_reader :ability + def initialize(ability) + @ability = ability + end + + # We ask the ability's runner for a score + def score(context) + context.runner(@ability).score + end + + def pass?(context) + context.allowed?(@ability) + end + + def cached_pass?(context) + runner = context.runner(@ability) + return nil unless runner.cached? + runner.pass? + end + + def description(context) + "User can #{@ability.inspect}" + end + + def repr + "can?(#{@ability.inspect})" + end + end + + # Logical `and`, containing a list of rules. Only passes + # if all of them do. + class And < Base + attr_reader :rules + def initialize(rules) + @rules = rules + end + + def simplify + simplified_rules = @rules.flat_map do |rule| + simplified = rule.simplify + case simplified + when And then simplified.rules + else [simplified] + end + end + + And.new(simplified_rules) + end + + def score(context) + return 0 unless cached_pass?(context).nil? + + # note that cached rules will have score 0 anyways. + @rules.map { |r| r.score(context) }.inject(0, :+) + end + + def pass?(context) + # try to find a cached answer before + # checking in order + cached = cached_pass?(context) + return cached unless cached.nil? + + @rules.all? { |r| r.pass?(context) } + end + + def cached_pass?(context) + passes = @rules.map { |r| r.cached_pass?(context) } + return false if passes.any? { |p| p == false } + return true if passes.all? { |p| p == true } + + nil + end + + def repr + "all?(#{rules.map(&:repr).join(', ')})" + end + end + + # Logical `or`. Mirrors And. + class Or < Base + attr_reader :rules + def initialize(rules) + @rules = rules + end + + def pass?(context) + cached = cached_pass?(context) + return cached unless cached.nil? + + @rules.any? { |r| r.pass?(context) } + end + + def simplify + simplified_rules = @rules.flat_map do |rule| + simplified = rule.simplify + case simplified + when Or then simplified.rules + else [simplified] + end + end + + Or.new(simplified_rules) + end + + def cached_pass?(context) + passes = @rules.map { |r| r.cached_pass?(context) } + return true if passes.any? { |p| p == true } + return false if passes.all? { |p| p == false } + + nil + end + + def score(context) + return 0 unless cached_pass?(context).nil? + @rules.map { |r| r.score(context) }.inject(0, :+) + end + + def repr + "any?(#{@rules.map(&:repr).join(', ')})" + end + end + + class Not < Base + attr_reader :rule + def initialize(rule) + @rule = rule + end + + def simplify + case @rule + when And then Or.new(@rule.rules.map(&:negate)).simplify + when Or then And.new(@rule.rules.map(&:negate)).simplify + when Not then @rule.rule.simplify + else Not.new(@rule.simplify) + end + end + + def pass?(context) + !@rule.pass?(context) + end + + def cached_pass?(context) + case @rule.cached_pass?(context) + when nil then nil + when true then false + when false then true + end + end + + def score(context) + @rule.score(context) + end + + def repr + "~#{@rule.repr}" + end + end + end +end diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb new file mode 100644 index 00000000000..b5c615da4e3 --- /dev/null +++ b/lib/declarative_policy/runner.rb @@ -0,0 +1,181 @@ +module DeclarativePolicy + class Runner + class State + def initialize + @enabled = false + @prevented = false + end + + def enable! + @enabled = true + end + + def enabled? + @enabled + end + + def prevent! + @prevented = true + end + + def prevented? + @prevented + end + + def pass? + !prevented? && enabled? + end + end + + # a Runner contains a list of Steps to be run. + attr_reader :steps + def initialize(steps) + @steps = steps + end + + # We make sure only to run any given Runner once, + # and just continue to use the resulting @state + # that's left behind. + def cached? + !!@state + end + + # used by Rule::Ability. See #steps_by_score + def score + return 0 if cached? + steps.map(&:score).inject(0, :+) + end + + def merge_runner(other) + Runner.new(@steps + other.steps) + end + + # The main entry point, called for making an ability decision. + # See #run and DeclarativePolicy::Base#can? + def pass? + run unless cached? + + @state.pass? + end + + # see DeclarativePolicy::Base#debug + def debug(out = $stderr) + run(out) + end + + private + + def flatten_steps! + @steps = @steps.flat_map { |s| s.flattened(@steps) } + end + + # This method implements the semantic of "one enable and no prevents". + # It relies on #steps_by_score for the main loop, and updates @state + # with the result of the step. + def run(debug = nil) + @state = State.new + + steps_by_score do |step, score| + passed = nil + case step.action + when :enable then + # we only check :enable actions if they have a chance of + # changing the outcome - if no other rule has enabled or + # prevented. + unless @state.enabled? || @state.prevented? + passed = step.pass? + @state.enable! if passed + end + + debug << inspect_step(step, score, passed) if debug + when :prevent then + # we only check :prevent actions if the state hasn't already + # been prevented. + unless @state.prevented? + passed = step.pass? + if passed + @state.prevent! + return unless debug + end + end + + debug << inspect_step(step, score, passed) if debug + else raise "invalid action #{step.action.inspect}" + end + end + + @state + end + + # This is the core spot where all those `#score` methods matter. + # It is critcal for performance to run steps in the correct order, + # so that we don't compute expensive conditions (potentially n times + # if we're called on, say, a large list of users). + # + # In order to determine the cheapest step to run next, we rely on + # Step#score, which returns a numerical rating of how expensive + # it would be to calculate - the lower the better. It would be + # easy enough to statically sort by these scores, but we can do + # a little better - the scores are cache-aware (conditions that + # are already in the cache have score 0), which means that running + # a step can actually change the scores of other steps. + # + # So! The way we sort here involves re-scoring at every step. This + # is by necessity quadratic, but most of the time the number of steps + # will be low. But just in case, if the number of steps exceeds 50, + # we print a warning and fall back to a static sort. + # + # For each step, we yield the step object along with the computed score + # for debugging purposes. + def steps_by_score(&b) + flatten_steps! + + if @steps.size > 50 + warn "DeclarativePolicy: large number of steps (#{steps.size}), falling back to static sort" + + @steps.map { |s| [s.score, s] }.sort_by { |(score, _)| score }.each do |(score, step)| + yield step, score + end + + return + end + + steps = Set.new(@steps) + + loop do + return if steps.empty? + + # if the permission hasn't yet been enabled and we only have + # prevent steps left, we short-circuit the state here + @state.prevent! if !@state.enabled? && steps.all?(&:prevent?) + + lowest_score = Float::INFINITY + next_step = nil + + steps.each do |step| + score = step.score + if score < lowest_score + next_step = step + lowest_score = score + end + end + + steps.delete(next_step) + + yield next_step, lowest_score + end + end + + # Formatter for debugging output. + def inspect_step(step, original_score, passed) + symbol = + case passed + when true then '+' + when false then '-' + when nil then ' ' + end + + "#{symbol} [#{original_score.to_i}] #{step.repr}\n" + end + end +end diff --git a/lib/declarative_policy/step.rb b/lib/declarative_policy/step.rb new file mode 100644 index 00000000000..3469fe9f991 --- /dev/null +++ b/lib/declarative_policy/step.rb @@ -0,0 +1,86 @@ +module DeclarativePolicy + # This object represents one step in the runtime decision of whether + # an ability is allowed. It contains a Rule and a context (instance + # of DeclarativePolicy::Base), which contains the user, the subject, + # and the cache. It also contains an "action", which is the symbol + # :prevent or :enable. + class Step + attr_reader :context, :rule, :action + def initialize(context, rule, action) + @context = context + @rule = rule + @action = action + end + + # In the flattening process, duplicate steps may be generated in the + # same rule. This allows us to eliminate those (see Runner#steps_by_score + # and note its use of a Set) + def ==(other) + @context == other.context && @rule == other.rule && @action == other.action + end + + # In the runner, steps are sorted dynamically by score, so that + # we are sure to compute them in close to the optimal order. + # + # See also Rule#score, ManifestCondition#score, and Runner#steps_by_score. + def score + # we slightly prefer the preventative actions + # since they are more likely to short-circuit + case @action + when :prevent + @rule.score(@context) * (7.0 / 8) + when :enable + @rule.score(@context) + end + end + + def with_action(action) + Step.new(@context, @rule, action) + end + + def enable? + @action == :enable + end + + def prevent? + @action == :prevent + end + + # This rather complex method allows us to split rules into parts so that + # they can be sorted independently for better optimization + def flattened(roots) + case @rule + when Rule::Or + # A single `Or` step is the same as each of its elements as separate steps + @rule.rules.flat_map { |r| Step.new(@context, r, @action).flattened(roots) } + when Rule::Ability + # This looks like a weird micro-optimization but it buys us quite a lot + # in some cases. If we depend on an Ability (i.e. a `can?(...)` rule), + # and that ability *only* has :enable actions (modulo some actions that + # we already have taken care of), then its rules can be safely inlined. + steps = @context.runner(@rule.ability).steps.reject { |s| roots.include?(s) } + + if steps.all?(&:enable?) + # in the case that we are a :prevent step, each inlined step becomes + # an independent :prevent, even though it was an :enable in its initial + # context. + steps.map! { |s| s.with_action(:prevent) } if prevent? + + steps.flat_map { |s| s.flattened(roots) } + else + [self] + end + else + [self] + end + end + + def pass? + @rule.pass?(@context) + end + + def repr + "#{@action} when #{@rule.repr} (#{@context.repr})" + end + end +end diff --git a/lib/feature.rb b/lib/feature.rb index d3d972564af..363f66ba60e 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -12,6 +12,8 @@ class Feature end class << self + delegate :group, to: :flipper + def all flipper.features.to_a end @@ -27,16 +29,24 @@ class Feature all.map(&:name).include?(feature.name) end - def enabled?(key) - get(key).enabled? + def enabled?(key, thing = nil) + get(key).enabled?(thing) + end + + def enable(key, thing = true) + get(key).enable(thing) + end + + def disable(key, thing = false) + get(key).disable(thing) end - def enable(key) - get(key).enable + def enable_group(key, group) + get(key).enable_group(group) end - def disable(key) - get(key).disable + def disable_group(key, group) + get(key).disable_group(group) end def flipper diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb index e4f7cad2b79..45c2b01dd8f 100644 --- a/lib/gitlab/allowable.rb +++ b/lib/gitlab/allowable.rb @@ -1,7 +1,7 @@ module Gitlab module Allowable - def can?(user, action, subject = :global) - Ability.allowed?(user, action, subject) + def can?(*args) + Ability.allowed?(*args) end end end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 0d5a7cf0694..d7dab584a44 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -93,7 +93,7 @@ module Gitlab row.values_at(*keys).map { |value| connection.quote(value) } end - connection.execute <<-EOF.strip_heredoc + connection.execute <<-EOF INSERT INTO #{table} (#{columns.join(', ')}) VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} EOF diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb index 89530082cd2..f333ff22300 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb @@ -29,6 +29,11 @@ module Gitlab paths = Array(paths) RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level) end + + def revert_renames + RenameProjects.new([], self).revert_renames + RenameNamespaces.new([], self).revert_renames + end end 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 d8163d7da11..33f8939bc61 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 @@ -6,7 +6,10 @@ module Gitlab attr_reader :paths, :migration delegate :update_column_in_batches, + :execute, :replace_sql, + :quote_string, + :say, to: :migration def initialize(paths, migration) @@ -26,24 +29,45 @@ module Gitlab new_path = rename_path(namespace_path, old_path) new_full_path = join_routable_path(namespace_path, new_path) + perform_rename(routable, old_full_path, new_full_path) + + [old_full_path, new_full_path] + end + + def perform_rename(routable, old_full_path, new_full_path) # skips callbacks & validations + new_path = new_full_path.split('/').last routable.class.where(id: routable) .update_all(path: new_path) rename_routes(old_full_path, new_full_path) - - [old_full_path, new_full_path] end def rename_routes(old_full_path, new_full_path) + routes = Route.arel_table + + quoted_old_full_path = quote_string(old_full_path) + quoted_old_wildcard_path = quote_string("#{old_full_path}/%") + + filter = if Database.mysql? + "lower(routes.path) = lower('#{quoted_old_full_path}') "\ + "OR routes.path LIKE '#{quoted_old_wildcard_path}'" + else + "routes.id IN "\ + "( SELECT routes.id FROM routes WHERE lower(routes.path) = lower('#{quoted_old_full_path}') "\ + "UNION SELECT routes.id FROM routes WHERE routes.path ILIKE '#{quoted_old_wildcard_path}' )" + end + replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path) - update_column_in_batches(:routes, :path, replace_statement) do |table, query| - path_or_children = table[:path].matches_any([old_full_path, "#{old_full_path}/%"]) - query.where(path_or_children) - end + update = Arel::UpdateManager.new(ActiveRecord::Base) + .table(routes) + .set([[routes[:path], replace_statement]]) + .where(Arel::Nodes::SqlLiteral.new(filter)) + + execute(update.to_sql) end def rename_path(namespace_path, path_was) @@ -86,32 +110,74 @@ module Gitlab def move_folders(directory, old_relative_path, new_relative_path) old_path = File.join(directory, old_relative_path) - return unless File.directory?(old_path) + unless File.directory?(old_path) + say "#{old_path} doesn't exist, skipping" + return + end new_path = File.join(directory, new_relative_path) FileUtils.mv(old_path, new_path) end def remove_cached_html_for_projects(project_ids) - update_column_in_batches(:projects, :description_html, nil) do |table, query| - query.where(table[:id].in(project_ids)) - end - - update_column_in_batches(:issues, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) + project_ids.each do |project_id| + update_column_in_batches(:projects, :description_html, nil) do |table, query| + query.where(table[:id].eq(project_id)) + end + + update_column_in_batches(:issues, :description_html, nil) do |table, query| + query.where(table[:project_id].eq(project_id)) + end + + update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| + query.where(table[:target_project_id].eq(project_id)) + end + + update_column_in_batches(:notes, :note_html, nil) do |table, query| + query.where(table[:project_id].eq(project_id)) + end + + update_column_in_batches(:milestones, :description_html, nil) do |table, query| + query.where(table[:project_id].eq(project_id)) + end end + end - update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| - query.where(table[:target_project_id].in(project_ids)) + def track_rename(type, old_path, new_path) + key = redis_key_for_type(type) + Gitlab::Redis.with do |redis| + redis.lpush(key, [old_path, new_path].to_json) + redis.expire(key, 2.weeks.to_i) end + say "tracked rename: #{key}: #{old_path} -> #{new_path}" + end - update_column_in_batches(:notes, :note_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) + def reverts_for_type(type) + key = redis_key_for_type(type) + + Gitlab::Redis.with do |redis| + failed_reverts = [] + + while rename_info = redis.lpop(key) + path_before_rename, path_after_rename = JSON.parse(rename_info) + say "renaming #{type} from #{path_after_rename} back to #{path_before_rename}" + begin + yield(path_before_rename, path_after_rename) + rescue StandardError => e + failed_reverts << rename_info + say "Renaming #{type} from #{path_after_rename} back to "\ + "#{path_before_rename} failed. Review the error and try "\ + "again by running the `down` action. \n"\ + "#{e.message}: \n #{e.backtrace.join("\n")}" + end + end + + failed_reverts.each { |rename_info| redis.lpush(key, rename_info) } end + end - update_column_in_batches(:milestones, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end + def redis_key_for_type(type) + "rename:#{migration.name}:#{type}" end def file_storage? 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 da7e2cb2e85..05b86f32ce2 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 @@ -26,6 +26,12 @@ module Gitlab def rename_namespace(namespace) old_full_path, new_full_path = rename_path_for_routable(namespace) + track_rename('namespace', old_full_path, new_full_path) + + rename_namespace_dependencies(namespace, old_full_path, new_full_path) + end + + def rename_namespace_dependencies(namespace, old_full_path, new_full_path) move_repositories(namespace, old_full_path, new_full_path) move_uploads(old_full_path, new_full_path) move_pages(old_full_path, new_full_path) @@ -33,6 +39,23 @@ module Gitlab remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id)) end + def revert_renames + reverts_for_type('namespace') do |path_before_rename, current_path| + matches_path = MigrationClasses::Route.arel_table[:path].matches(current_path) + namespace = MigrationClasses::Namespace.joins(:route) + .where(matches_path).first&.becomes(MigrationClasses::Namespace) + + if namespace + perform_rename(namespace, current_path, path_before_rename) + + rename_namespace_dependencies(namespace, current_path, path_before_rename) + else + say "Couldn't rename namespace from #{current_path} back to #{path_before_rename}, "\ + "namespace was renamed, or no longer exists at the expected path" + end + end + end + def rename_user(old_username, new_username) MigrationClasses::User.where(username: old_username) .update_all(username: new_username) diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb index 448717eb744..75a75f61953 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb @@ -16,12 +16,37 @@ module Gitlab def rename_project(project) old_full_path, new_full_path = rename_path_for_routable(project) + track_rename('project', old_full_path, new_full_path) + + move_project_folders(project, old_full_path, new_full_path) + end + + def move_project_folders(project, old_full_path, new_full_path) move_repository(project, old_full_path, new_full_path) move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") move_uploads(old_full_path, new_full_path) move_pages(old_full_path, new_full_path) end + def revert_renames + reverts_for_type('project') do |path_before_rename, current_path| + matches_path = MigrationClasses::Route.arel_table[:path].matches(current_path) + project = MigrationClasses::Project.joins(:route) + .where(matches_path).first + + if project + perform_rename(project, current_path, path_before_rename) + + move_project_folders(project, current_path, path_before_rename) + else + say "Couldn't rename project from #{current_path} back to "\ + "#{path_before_rename}, project was renamed or no longer "\ + "exists at the expected path." + + end + end + end + def move_repository(project, old_path, new_path) unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, diff --git a/lib/gitlab/database/sha_attribute.rb b/lib/gitlab/database/sha_attribute.rb new file mode 100644 index 00000000000..d9400e04b83 --- /dev/null +++ b/lib/gitlab/database/sha_attribute.rb @@ -0,0 +1,34 @@ +module Gitlab + module Database + BINARY_TYPE = if Gitlab::Database.postgresql? + # PostgreSQL defines its own class with slightly different + # behaviour from the default Binary type. + ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea + else + ActiveRecord::Type::Binary + end + + # Class for casting binary data to hexadecimal SHA1 hashes (and vice-versa). + # + # Using ShaAttribute allows you to store SHA1 values as binary while still + # using them as if they were stored as string values. This gives you the + # ease of use of string values, but without the storage overhead. + class ShaAttribute < BINARY_TYPE + PACK_FORMAT = 'H*'.freeze + + # Casts binary data to a SHA1 in hexadecimal. + def type_cast_from_database(value) + value = super + + value ? value.unpack(PACK_FORMAT)[0] : nil + end + + # Casts a SHA1 in hexadecimal to the proper binary format. + def type_cast_for_database(value) + arg = value ? [value].pack(PACK_FORMAT) : nil + + super(arg) + end + end + end +end diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index 7bbd154eb03..d2360583741 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -52,7 +52,7 @@ module Gitlab # # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"` def link_regex(regex, &url_proc) highlighted_lines.map!.with_index do |rich_line, i| - marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe) + marker = StringRegexMarker.new(plain_lines[i].chomp, rich_line.html_safe) marker.mark(regex, group: :name) do |text, left:, right:| url = yield(text) 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/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/git.rb b/lib/gitlab/git.rb index 936606152e9..4175746be39 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -7,8 +7,10 @@ module Gitlab CommandError = Class.new(StandardError) class << self + include Gitlab::EncodingHelper + def ref_name(ref) - ref.sub(/\Arefs\/(tags|heads)\//, '') + encode! ref.sub(/\Arefs\/(tags|heads)\//, '') end def branch_name(ref) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index d5d149f1423..9c0606d780a 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -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/hook.rb b/lib/gitlab/git/hook.rb index bd90d24a2ec..5042916343b 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -4,9 +4,10 @@ module Gitlab GL_PROTOCOL = 'web'.freeze attr_reader :name, :repo_path, :path - def initialize(name, repo_path) + def initialize(name, project) @name = name - @repo_path = repo_path + @project = project + @repo_path = project.repository.path @path = File.join(repo_path.strip, 'hooks', name) end @@ -38,7 +39,8 @@ module Gitlab vars = { 'GL_ID' => gl_id, 'PWD' => repo_path, - 'GL_PROTOCOL' => GL_PROTOCOL + 'GL_PROTOCOL' => GL_PROTOCOL, + 'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false) } options = { diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index c1f942f931a..23d0c8a9bdb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -113,9 +113,7 @@ module Gitlab def local_branches(sort_by: nil) gitaly_migrate(:local_branches) do |is_enabled| if is_enabled - gitaly_ref_client.local_branches(sort_by: sort_by).map do |gitaly_branch| - Gitlab::Git::Branch.new(self, gitaly_branch.name, gitaly_branch) - end + gitaly_ref_client.local_branches(sort_by: sort_by) else branches(filter: :local, sort_by: sort_by) end @@ -494,70 +492,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,32 +547,20 @@ module Gitlab rugged.rev_parse(oid_or_ref_name) end - # Return hash with submodules info for this repository + # Returns url for submodule # # Ex. - # { - # "current_path/rack" => { - # "name" => "original_path/rack", - # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320", - # "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 - - parser = GitmodulesParser.new(content) - fill_submodule_ids(commit, parser.parse) end # Return total commits count accessible from passed ref @@ -976,6 +898,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 @@ -1228,20 +1167,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/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index 6d5f54dd959..2d61992f595 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -1,8 +1,11 @@ module Gitlab module GitalyClient class Ref + include Gitlab::EncodingHelper + # 'repository' is a Gitlab::Git::Repository def initialize(repository) + @repository = repository @gitaly_repo = repository.gitaly_repository @storage = repository.storage end @@ -16,13 +19,13 @@ module Gitlab def branch_names request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo) response = GitalyClient.call(@storage, :ref, :find_all_branch_names, request) - consume_refs_response(response, prefix: 'refs/heads/') + consume_refs_response(response) { |name| Gitlab::Git.branch_name(name) } end def tag_names request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo) response = GitalyClient.call(@storage, :ref, :find_all_tag_names, request) - consume_refs_response(response, prefix: 'refs/tags/') + consume_refs_response(response) { |name| Gitlab::Git.tag_name(name) } end def find_ref_name(commit_id, ref_prefix) @@ -51,20 +54,28 @@ module Gitlab private - def consume_refs_response(response, prefix:) - response.flat_map do |r| - r.names.map { |name| name.sub(/\A#{Regexp.escape(prefix)}/, '') } - end + def consume_refs_response(response) + response.flat_map { |message| message.names.map { |name| yield(name) } } end def sort_by_param(sort_by) + sort_by = 'name' if sort_by == 'name_asc' + enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym) raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value enum_value end def consume_branches_response(response) - response.flat_map { |r| r.branches } + response.flat_map do |message| + message.branches.map do |gitaly_branch| + Gitlab::Git::Branch.new( + @repository, + encode!(gitaly_branch.name.dup), + gitaly_branch.commit_id + ) + end + end end end end diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index a5ad2f952d3..db7cdf4b5c7 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -11,7 +11,8 @@ module Gitlab 'zh_CN' => '简体中文', 'zh_HK' => '繁體中文(香港)', 'zh_TW' => '繁體中文(臺灣)', - 'bg' => 'български' + 'bg' => 'български', + 'eo' => 'Esperanto' }.freeze def available_locales diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 72183e8aad4..1860352c96d 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -99,6 +99,7 @@ excluded_attributes: - :milestone_id merge_requests: - :milestone_id + - :ref_fetched award_emoji: - :awardable_id statuses: diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 54a5b1d31cd..fb68627dedf 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_at: Time.now).execute + true else false 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/usage_data.rb b/lib/gitlab/usage_data.rb index bcba2e3e1b6..f19b325a126 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -20,13 +20,15 @@ module Gitlab counts: { boards: Board.count, ci_builds: ::Ci::Build.count, - ci_pipelines: ::Ci::Pipeline.count, + ci_internal_pipelines: ::Ci::Pipeline.internal.count, + ci_external_pipelines: ::Ci::Pipeline.external.count, ci_runners: ::Ci::Runner.count, ci_triggers: ::Ci::Trigger.count, ci_pipeline_schedules: ::Ci::PipelineSchedule.count, 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/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb index dbfe0941e4d..841fb681435 100644 --- a/lib/gitlab/view/presenter/base.rb +++ b/lib/gitlab/view/presenter/base.rb @@ -15,6 +15,11 @@ module Gitlab super(user, action, overriden_subject || subject) end + # delegate all #can? queries to the subject + def declarative_policy_delegate + subject + end + class_methods do def presenter? true 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/locale/bg/gitlab.po b/locale/bg/gitlab.po index 43a5de65c43..dd1430700f8 100644 --- a/locale/bg/gitlab.po +++ b/locale/bg/gitlab.po @@ -1,27 +1,245 @@ +# 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-05-04 19:24-0500\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-05 09:40-0400\n" +"PO-Revision-Date: 2017-06-20 06:26-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 "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 "Добавяне на „%{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 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 +268,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 +366,302 @@ 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 "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] "Ограничено до показване на последното %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 "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 "Етап" +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 +675,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 %{new_merge_request} with these changes" +msgstr "Създайте %{new_merge_request} с тези промени" + +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 +768,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 +785,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 +808,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 +846,8 @@ msgid "" "first pipeline finishes running." msgstr "" "Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни " -"всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени " -"автоматично след като приключи изпълнените на първата Ви такава задача." +"всяка схема от задачи за свързаната заявка за сливане. Данните ще бъдат " +"добавени автоматично след като приключи изпълнението на първата Ви схема." msgid "The time taken by each data entry gathered by that stage." msgstr "Времето, което отнема всеки запис от данни за съответния етап." @@ -212,6 +861,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 +881,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,20 +1023,119 @@ 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 "You have reached your project limit" +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 "Нуждаете се от разрешение." +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 "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/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index f0a9e44daf3..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,18 +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 "" +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 5130572d7ed..bb2b84c67b0 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -1,128 +1,460 @@ -# 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 +# Lin Jen-Shin <anonymous@domain.com>, 2017. +# Hazel Yang <anonymous@domain.com>, 2017. +# TzeKei Lee <anonymous@domain.com>, 2017. +# Jerry Ho <a29988122@gmail.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 (Taiwan) (https://www.transifex.com/gitlab-zh/teams/751" -"77/zh_TW/)\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_TW\n" -"Plural-Forms: nplurals=1; plural=0;\n" +"PO-Revision-Date: 2017-06-28 11:13-0400\n" +"Last-Translator: Huang Tao <htve@outlook.com>\n" +"Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n" +"Language: zh-TW\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 金鑰到您的個人帳號,才能使用 SSH 來上傳 (push) 或下載 (pull) 。" + +msgid "Add new directory" +msgstr "新增目錄" + +msgid "Archived project! Repository is read-only" +msgstr "此專案已封存!檔案庫 (repository) 為唯讀狀態" msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "確定要刪除此流水線 (pipeline) 排程嗎?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "拖放檔案到此處或者 %{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "分支 (branch) " + +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 "" +"已建立分支 (branch) <strong>%{branch_name}</strong> 。如要設定自動部署, 請選擇合適的 GitLab CI " +"Yaml 模板,然後記得要送交 (commit) 您的編輯內容。%{link_to_autodeploy_doc}\n" + +msgid "Branches" +msgstr "分支 (branch) " + +msgid "Browse files" +msgstr "瀏覽檔案" msgid "ByAuthor|by" -msgstr "作者:" +msgstr "作者:" + +msgid "CI configuration" +msgstr "CI 組態" msgid "Cancel" -msgstr "" +msgstr "取消" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "挑選到分支 (branch) " + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "還原分支 (branch) " + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "挑選" + +msgid "ChangeTypeAction|Revert" +msgstr "還原" + +msgid "Changelog" +msgstr "更新日誌" + +msgid "Charts" +msgstr "統計圖" + +msgid "Cherry-pick this commit" +msgstr "挑選此更動記錄 (commit) " + +msgid "Cherry-pick this merge request" +msgstr "挑選此合併請求 (merge request) " + +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[0] "更動記錄 (commit) " + +msgid "Commit message" +msgstr "更動說明 (commit) " + +msgid "CommitBoxTitle|Commit" +msgstr "送交" + +msgid "CommitMessage|Add %{file_name}" +msgstr "建立 %{file_name}" + +msgid "Commits" +msgstr "更動記錄 (commit) " + +msgid "Commits|History" +msgstr "過去更動 (commit) " + +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 "複製更動記錄 (commit) 的 SHA 值到剪貼簿" + +msgid "Create New Directory" +msgstr "建立新目錄" + +msgid "Create directory" +msgstr "建立目錄" + +msgid "Create empty bare repository" +msgstr "建立一個新的 bare repository" + +msgid "Create merge request" +msgstr "發出合併請求 (merge request) " + +msgid "Create new..." +msgstr "建立..." + +msgid "CreateNewFork|Fork" +msgstr "分支 (fork) " + +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 gives an overview of how much time it takes to go from idea to production in your project." -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 "週期分析讓您可以有效的釐清專案從發想到產品推出所花的時間長短。" msgid "CycleAnalyticsStage|Code" msgstr "程式開發" msgid "CycleAnalyticsStage|Issue" -msgstr "議題" +msgstr "議題 (issue) " msgid "CycleAnalyticsStage|Plan" msgstr "計劃" msgid "CycleAnalyticsStage|Production" -msgstr "上線" +msgstr "營運" msgid "CycleAnalyticsStage|Review" msgstr "複閱" msgid "CycleAnalyticsStage|Staging" -msgstr "預備" +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 "電子郵件修補檔案 (patch)" + +msgid "DownloadCommit|Plain Diff" +msgstr "差異檔 (diff)" + +msgid "DownloadSource|Download" +msgstr "下載原始碼" msgid "Edit" -msgstr "" +msgstr "編輯" msgid "Edit Pipeline Schedule %{id}" -msgstr "" +msgstr "編輯 %{id} 流水線 (pipeline) 排程" + +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 "" +msgstr "無法變更所有權" msgid "Failed to remove the pipeline schedule" -msgstr "" +msgstr "無法刪除流水線 (pipeline) 排程" -msgid "Filter" -msgstr "" +msgid "Files" +msgstr "檔案" + +msgid "Find by path" +msgstr "以路徑搜尋" + +msgid "Find file" +msgstr "搜尋檔案" msgid "FirstPushedBy|First" -msgstr "首次推送" +msgstr "首次推送 (push) " msgid "FirstPushedBy|pushed by" -msgstr "推送者:" +msgstr "推送者 (push) :" + +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "分支 (fork) " + +msgid "ForkedFromProjectPath|Forked from" +msgstr "分支 (fork) 自" msgid "From issue creation until deploy to production" -msgstr "從議題建立至線上部署" +msgstr "從議題 (issue) 建立直到部署至營運環境" msgid "From merge request merge until deploy to production" -msgstr "從請求被合併後至線上部署" +msgstr "從請求被合併後 (merge request merged) 直到部署至營運環境" + +msgid "Go to your fork" +msgstr "前往您的分支 (fork) " + +msgid "GoToYourFork|Fork" +msgstr "前往您的分支 (fork) " + +msgid "Home" +msgstr "首頁" + +msgid "Housekeeping successfully started" +msgstr "已開始維護" + +msgid "Import repository" +msgstr "匯入檔案庫 (repository)" 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 "最新流水線 (pipeline) " + +msgid "Last Update" +msgstr "最後更新" + +msgid "Last commit" +msgstr "最後更動記錄 (commit) " + +msgid "Learn more in the" +msgstr "了解更多" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "流水線 (pipeline) 排程說明文件" + +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[0] "限制最多顯示 %d 個事件" msgid "Median" msgstr "中位數" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "新增 SSH 金鑰" + msgid "New Issue" msgid_plural "New Issues" -msgstr[0] "新議題" +msgstr[0] "建立議題 (issue) " msgid "New Pipeline Schedule" -msgstr "" +msgstr "建立流水線 (pipeline) 排程" + +msgid "New branch" +msgstr "新分支 (branch) " + +msgid "New directory" +msgstr "新增目錄" + +msgid "New file" +msgstr "新增檔案" + +msgid "New issue" +msgstr "新增議題 (issue) " + +msgid "New merge request" +msgstr "新增合併請求 (merge request) " + +msgid "New schedule" +msgstr "新增排程" + +msgid "New snippet" +msgstr "新文字片段" + +msgid "New tag" +msgstr "新增標籤" + +msgid "No repository" +msgstr "找不到檔案庫 (repository)" msgid "No schedules" -msgstr "" +msgstr "沒有排程" msgid "Not available" msgstr "無法使用" @@ -130,135 +462,502 @@ msgstr "無法使用" msgid "Not enough data" msgstr "資料不足" +msgid "Notification events" +msgstr "事件通知" + +msgid "NotificationEvent|Close issue" +msgstr "關閉議題 (issue) " + +msgid "NotificationEvent|Close merge request" +msgstr "關閉合併請求 (merge request) " + +msgid "NotificationEvent|Failed pipeline" +msgstr "流水線 (pipeline) 失敗" + +msgid "NotificationEvent|Merge merge request" +msgstr "合併請求 (merge request) 被合併" + +msgid "NotificationEvent|New issue" +msgstr "新增議題 (issue) " + +msgid "NotificationEvent|New merge request" +msgstr "新增合併請求 (merge request) " + +msgid "NotificationEvent|New note" +msgstr "新增評論" + +msgid "NotificationEvent|Reassign issue" +msgstr "重新指派議題 (issue) " + +msgid "NotificationEvent|Reassign merge request" +msgstr "重新指派合併請求 (merge request) " + +msgid "NotificationEvent|Reopen issue" +msgstr "重啟議題 (issue)" + +msgid "NotificationEvent|Successful pipeline" +msgstr "流水線 (pipeline) 成功完成" + +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 "流水線 (pipeline) " msgid "Pipeline Health" -msgstr "流水線健康指標" +msgstr "流水線 (pipeline) 健康指數" msgid "Pipeline Schedule" -msgstr "" +msgstr "流水線 (pipeline) 排程" msgid "Pipeline Schedules" -msgstr "" +msgstr "流水線 (pipeline) 排程" 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 "請簡單說明此流水線 (pipeline) " 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 "了解更多" +msgstr "瞭解更多" + +msgid "Readme" +msgstr "說明檔" + +msgid "RefSwitcher|Branches" +msgstr "分支 (branch) " + +msgid "RefSwitcher|Tags" +msgstr "標籤" msgid "Related Commits" -msgstr "相關的送交" +msgstr "相關的更動記錄 (commit) " msgid "Related Deployed Jobs" msgstr "相關的部署作業" msgid "Related Issues" -msgstr "相關的議題" +msgstr "相關的議題 (issue) " msgid "Related Jobs" msgstr "相關的作業" msgid "Related Merge Requests" -msgstr "相關的合併請求" +msgstr "相關的合併請求 (merge request) " msgid "Related Merged Requests" msgstr "相關已合併的請求" +msgid "Remind later" +msgstr "稍後提醒" + +msgid "Remove project" +msgstr "刪除專案" + +msgid "Request Access" +msgstr "申請權限" + +msgid "Revert this commit" +msgstr "還原此更動記錄 (commit)" + +msgid "Revert this merge request" +msgstr "還原此合併請求 (merge request) " + msgid "Save pipeline schedule" -msgstr "" +msgstr "保存流水線 (pipeline) 排程" msgid "Schedule a new pipeline" -msgstr "" +msgstr "建立流水線 (pipeline) 排程" + +msgid "Scheduling Pipelines" +msgstr "流水線 (pipeline) 計劃" + +msgid "Search branches and tags" +msgstr "搜尋分支 (branch) 和標籤" + +msgid "Select Archive Format" +msgstr "選擇下載格式" msgid "Select a timezone" -msgstr "" +msgstr "選擇時區" msgid "Select target branch" -msgstr "" +msgstr "選擇目標分支 (branch) " + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "請先設定密碼,才能使用 %{protocol} 來上傳 (push) 或下載 (pull) 。" + +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 "切換分支 (branch) 或標籤" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "標籤" + +msgid "Tags" +msgstr "標籤" + msgid "Target Branch" -msgstr "" +msgstr "目標分支 (branch) " -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 "" +"程式開發階段顯示從第一次更動記錄 (commit) 到建立合併請求 (merge request) 的時間。建立第一個合併請求後,資料將自動填入。" msgid "The collection of events added to the data gathered for that stage." -msgstr "與該階段相關的事件。" +msgstr "該階段中的相關事件集合。" + +msgid "The fork relationship has been removed." +msgstr "分支與主幹間的關聯 (fork relationship) 已被刪除。" -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 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 "" +"議題 (issue) 階段顯示從議題建立到設定里程碑所花的時間,或是議題被分類到議題看板 (issue board) " +"中所花的時間。建立第一個議題後,資料將自動填入。" msgid "The phase of the development lifecycle." -msgstr "專案開發生命週期的各個階段。" +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 "" +"在指定了特定分支 (branch) 或標籤後,此處的流水線 (pipeline) 排程會不斷地重複執行。\n" +"流水線排程的存取權限與專案本身相同。" -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 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 "計劃階段顯示從更動記錄 (commit) 被排程至第一個推送的時間。第一次推送之後,資料將自動填入。" + +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 "營運階段顯示從建立議題 (issue) 到部署程式上線所花的時間。完成從發想到上線的完整開發週期後,資料將自動填入。" -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 repository for this project does not exist." +msgstr "本專案沒有檔案庫 (repository) " + +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 "" +"複閱階段顯示從合併請求 (merge request) 建立後至被合併的時間。當建立第一個合併請求 (merge request) 後,資料將自動填入。" -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 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 "試營運段顯示從合併請求 (merge request) 被合併後至部署營運的時間。當第一次部署營運後,資料將自動填入" -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 "測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。" +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 "" +"測試階段顯示相關合併請求 (merge request) 的流水線 (pipeline) 所花的時間。當第一個流水線 (pipeline) " +"執行完畢後,資料將自動填入。" msgid "The time taken by each data entry gathered by that stage." -msgstr "每筆該階段相關資料所花的時間。" +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 "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個現存的檔案庫之前,您將無法上傳更新 (push) 。" + msgid "Time before an issue gets scheduled" -msgstr "議題等待排程的時間" +msgstr "議題 (issue) 被列入日程表的時間" msgid "Time before an issue starts implementation" -msgstr "議題等待開始實作的時間" +msgstr "議題 (issue) 等待開始實作的時間" msgid "Time between merge request creation and merge/close" -msgstr "合併請求被合併或是關閉的時間" +msgstr "合併請求 (merge request) 從建立到被合併或是關閉的時間" msgid "Time until first merge request" -msgstr "第一個合併請求被建立前的時間" +msgstr "第一個合併請求 (merge request) 被建立前的時間" + +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" @@ -275,7 +974,28 @@ msgid "Total Time" msgstr "總時間" msgid "Total test time for all commits/merges" -msgstr "所有送交和合併的總測試時間" +msgstr "合併 (merge) 與更動記錄 (commit) 的總測試時間" + +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 "權限不足。如需查看相關資料,請向管理員申請權限。" @@ -283,12 +1003,85 @@ msgstr "權限不足。如需查看相關資料,請向管理員申請權限。 msgid "We don't have enough data to show this stage." msgstr "因該階段的資料不足而無法顯示相關資訊" -msgid "You have reached your project limit" +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" +"真的「100%確定」要這麼做嗎?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" +"將要刪除本分支專案與主幹的所有關聯 (fork relationship) 。 %{forked_from_project} " +"真的「100%確定」要這麼做嗎?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "將要把 %{project_name_with_namespace} 的所有權轉移給另一個人。真的「100%確定」要這麼做嗎?" + +msgid "You can only add files when you are on a branch" +msgstr "只能在分支 (branch) 上建立檔案" + +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} 上傳 (push) 或下載 (pull) 程式碼。" + +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 上傳 (push) 或下載 (pull) 程式碼。" + +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/qa/Dockerfile b/qa/Dockerfile index 97ae1961e34..f3a81a7e355 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,8 +1,6 @@ FROM ruby:2.3 LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" - -ENV CHROME_VERSION 59.0.3071.109-1 -ENV CHROME_DRIVER_VERSION 2.30 +ENV DEBIAN_FRONTEND noninteractive ## # Update APT sources and install some dependencies @@ -15,22 +13,17 @@ RUN apt-get update && apt-get install -y wget git unzip xvfb # 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 && DEBIAN_FRONTEND=noninteractive apt-get install -y google-chrome-stable=$CHROME_VERSION +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/$CHROME_DRIVER_VERSION/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/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb index 80a418feb3e..ada011e7595 100644 --- a/spec/controllers/abuse_reports_controller_spec.rb +++ b/spec/controllers/abuse_reports_controller_spec.rb @@ -13,6 +13,31 @@ describe AbuseReportsController do sign_in(reporter) end + describe 'GET new' do + context 'when the user has already been deleted' do + it 'redirects the reporter to root_path' do + user_id = user.id + user.destroy + + get :new, { user_id: user_id } + + expect(response).to redirect_to root_path + expect(flash[:alert]).to eq('Cannot create the abuse report. The user has been deleted.') + end + end + + context 'when the user has already been blocked' do + it 'redirects the reporter to the user\'s profile' do + user.block + + get :new, { user_id: user.id } + + expect(response).to redirect_to user + expect(flash[:alert]).to eq('Cannot create the abuse report. This user has been blocked.') + end + end + end + describe 'POST create' do context 'with valid attributes' do it 'saves the abuse report' do 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 c20cf6a4291..561bc219bb4 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -235,7 +235,7 @@ describe Projects::BlobController do put :update, default_params expect(response).to redirect_to( - new_namespace_project_merge_request_path( + namespace_project_new_merge_request_path( forked_project.namespace, forked_project, merge_request: { diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb new file mode 100644 index 00000000000..9278ac8edd8 --- /dev/null +++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb @@ -0,0 +1,307 @@ +require 'spec_helper' + +describe Projects::MergeRequests::ConflictsController do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + let(:merge_request_with_conflicts) do + create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr| + mr.mark_as_unmergeable + end + end + + before do + sign_in(user) + end + + describe 'GET show' 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) + + get :show, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + format: 'json' + end + + it 'returns a 200 status code' do + expect(response).to have_http_status(:ok) + end + + it 'returns JSON with a message' do + expect(json_response.keys).to contain_exactly('message', 'type') + end + end + + context 'with valid conflicts' do + before do + get :show, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + format: 'json' + end + + it 'matches the schema' do + expect(response).to match_response_schema('conflicts') + end + + it 'includes meta info about the MR' do + expect(json_response['commit_message']).to include('Merge branch') + expect(json_response['commit_sha']).to match(/\h{40}/) + expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch) + expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch) + end + + it 'includes each file that has conflicts' do + filenames = json_response['files'].map { |file| file['new_path'] } + + expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb') + end + + it 'splits files into sections with lines' do + json_response['files'].each do |file| + file['sections'].each do |section| + expect(section).to include('conflict', 'lines') + + section['lines'].each do |line| + if section['conflict'] + expect(line['type']).to be_in(%w(old new)) + expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer)) + else + if line['type'].nil? + expect(line['old_line']).not_to eq(nil) + expect(line['new_line']).not_to eq(nil) + else + expect(line['type']).to eq('match') + expect(line['old_line']).to eq(nil) + expect(line['new_line']).to eq(nil) + end + end + end + end + end + end + + it 'has unique section IDs across files' do + section_ids = json_response['files'].flat_map do |file| + file['sections'].map { |section| section['id'] }.compact + end + + expect(section_ids.uniq).to eq(section_ids) + end + end + end + + describe 'GET conflict_for_path' do + def conflict_for_path(path) + get :conflict_for_path, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + old_path: path, + new_path: path, + format: 'json' + end + + 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) + + conflict_for_path('files/ruby/regex.rb') + end + + it 'returns a 404 status code' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when the file does not exist cannot be resolved in the UI' do + 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) + end + end + + context 'with an existing file' do + let(:path) { 'files/ruby/regex.rb' } + + 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 + + expect(json_response).to include('old_path' => path, + 'new_path' => path, + 'blob_icon' => 'file-text-o', + 'blob_path' => a_string_ending_with(path), + 'blob_ace_mode' => 'ruby', + 'content' => content) + end + end + end + + context 'POST resolve_conflicts' do + let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } + + def resolve_conflicts(files) + post :resolve_conflicts, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project, + id: merge_request_with_conflicts.iid, + format: 'json', + files: files, + commit_message: 'Commit message' + end + + context 'with valid params' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'creates a new commit on the branch' do + expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha) + expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message') + end + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + end + + context 'when sections are missing' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the name of the first missing section' do + expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + context 'when files are missing' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the name of the missing file' do + expect(json_response['message']).to include('files/ruby/popen.rb') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + 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 + + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'content' => content + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the path of the problem file' do + expect(json_response['message']).to include('files/ruby/popen.rb') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + end +end diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb new file mode 100644 index 00000000000..f9d8f0f5fcf --- /dev/null +++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Projects::MergeRequests::CreationsController do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:fork_project) { create(:forked_project_with_submodules) } + + before do + fork_project.team << [user, :master] + + sign_in(user) + end + + describe 'GET new' do + context 'merge request that removes a submodule' do + render_views + + it 'renders new merge request widget template' do + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + } + + expect(response).to be_success + end + end + end + + describe 'GET pipelines' do + before do + create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id, + ref: 'remove-submodule', + project: fork_project) + end + + it 'renders JSON including serialized pipelines' do + get :pipelines, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + }, + format: :json + + expect(response).to be_ok + expect(json_response).to have_key 'pipelines' + expect(json_response['pipelines']).not_to be_empty + end + end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + format: 'json' + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { 'files/ruby/feature.rb' } + + context 'when both branches are in the same project' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the source branch is in a different project to the target' do + let(:other_project) { create(:project) } + + before do + other_project.team << [user, :master] + end + + context 'when the path exists in the diff' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the path does not exist in the diff' do + 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) + end + end + end + end +end diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb new file mode 100644 index 00000000000..53fe2bdb189 --- /dev/null +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' + +describe Projects::MergeRequests::DiffsController do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } + + before do + sign_in(user) + end + + describe 'GET show' do + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + format: 'json' + } + + get :show, params.merge(extra_params) + end + + context 'with default params' do + context 'for the same project' do + before do + go + end + + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/diffs/_diffs') + expect(json_response).to have_key('html') + end + end + + context 'with forked projects with submodules' do + render_views + + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + merge_request.reload + go + end + + it 'renders' do + expect(response).to be_success + expect(response.body).to have_content('Subproject commit') + end + end + end + + context 'with ignore_whitespace_change' do + before do + go(w: 1) + end + + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/diffs/_diffs') + expect(json_response).to have_key('html') + end + end + + context 'with view' do + before do + go(view: 'parallel') + end + + it 'saves the preferred diff view in a cookie' do + expect(response.cookies['diff_view']).to eq('parallel') + end + end + end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + format: 'json' + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { 'files/ruby/popen.rb' } + + context 'when the merge request exists' do + context 'when the user can view the merge request' do + context 'when the path exists in the diff' do + it 'enables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_falsey + expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest', + noteable_id: merge_request.id) + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| + expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs) + end + + diff_for_path(old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before do + diff_for_path(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) + end + end + end + + context 'when the user cannot view the merge request' do + before do + project.team.truncate + diff_for_path(old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the merge request does not exist' do + 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) + end + end + + context 'when the merge request belongs to a different project' do + let(:other_project) { create(:empty_project) } + + before do + other_project.team << [user, :master] + diff_for_path(old_path: existing_path, new_path: existing_path, project_id: other_project) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6817c2652fd..6f9ce60cf75 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -14,53 +14,6 @@ describe Projects::MergeRequestsController do sign_in(user) end - describe 'GET new' do - context 'merge request that removes a submodule' do - render_views - - let(:fork_project) { create(:forked_project_with_submodules) } - - before do - fork_project.team << [user, :master] - end - - context 'when rendering HTML response' do - it 'renders new merge request widget template' do - submit_new_merge_request - - expect(response).to be_success - end - end - - context 'when rendering JSON response' do - before do - create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id, - ref: 'remove-submodule', - project: fork_project) - end - - it 'renders JSON including serialized pipelines' do - submit_new_merge_request(format: :json) - - expect(response).to be_ok - expect(json_response).to have_key 'pipelines' - expect(json_response['pipelines']).not_to be_empty - end - end - end - - def submit_new_merge_request(format: :html) - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - }, - format: format - end - end - describe 'GET commit_change_content' do it 'renders commit_change_content template' do get :commit_change_content, @@ -497,234 +450,6 @@ describe Projects::MergeRequestsController do end end - describe 'GET diffs' do - def go(extra_params = {}) - params = { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - } - - get :diffs, params.merge(extra_params) - end - - it_behaves_like "loads labels", :diffs - - context 'with default params' do - context 'as html' do - before do - go(format: 'html') - end - - it 'renders the diff template' do - expect(response).to render_template('diffs') - end - end - - context 'as json' do - 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') - expect(json_response).to have_key('html') - end - end - - context 'with forked projects with submodules' do - render_views - - let(:project) { create(:project) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } - - before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - merge_request.reload - go(format: 'json') - end - - it 'renders' do - expect(response).to be_success - expect(response.body).to have_content('Subproject commit') - end - end - end - - context 'with ignore_whitespace_change' do - context 'as html' do - before do - go(format: 'html', w: 1) - end - - it 'renders the diff template' do - expect(response).to render_template('diffs') - end - end - - context 'as json' do - 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') - expect(json_response).to have_key('html') - end - end - end - - context 'with view' do - before do - go(view: 'parallel') - end - - it 'saves the preferred diff view in a cookie' do - expect(response.cookies['diff_view']).to eq('parallel') - end - end - end - - describe 'GET diff_for_path' do - def diff_for_path(extra_params = {}) - params = { - namespace_id: project.namespace.to_param, - project_id: project - } - - get :diff_for_path, params.merge(extra_params) - end - - context 'when an ID param is passed' do - let(:existing_path) { 'files/ruby/popen.rb' } - - context 'when the merge request exists' do - context 'when the user can view the merge request' do - context 'when the path exists in the diff' do - it 'enables diff notes' do - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) - - expect(assigns(:diff_notes_disabled)).to be_falsey - expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest', - noteable_id: merge_request.id) - end - - it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| - expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs) - end - - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) - end - end - - context 'when the path does not exist in the diff' do - 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) - end - end - end - - context 'when the user cannot view the merge request' do - before do - project.team.truncate - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) - end - - it 'returns a 404' do - expect(response).to have_http_status(404) - end - end - end - - context 'when the merge request does not exist' do - 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) - end - end - - context 'when the merge request belongs to a different project' do - let(:other_project) { create(:empty_project) } - - before do - other_project.team << [user, :master] - diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project) - end - - it 'returns a 404' do - expect(response).to have_http_status(404) - end - end - end - - context 'when source and target params are passed' do - let(:existing_path) { 'files/ruby/feature.rb' } - - context 'when both branches are in the same project' do - it 'disables diff notes' do - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) - - expect(assigns(:diff_notes_disabled)).to be_truthy - end - - it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| - expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs) - end - - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) - end - end - - context 'when the source branch is in a different project to the target' do - let(:other_project) { create(:project) } - - before do - other_project.team << [user, :master] - end - - context 'when the path exists in the diff' do - it 'disables diff notes' do - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) - - expect(assigns(:diff_notes_disabled)).to be_truthy - end - - it 'only renders the diffs for the path given' do - expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs| - expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path) - meth.call(diffs) - end - - diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) - end - end - - context 'when the path does not exist in the diff' do - 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) - end - end - end - end - end - describe 'GET commits' do def go(format: 'html') get :commits, @@ -734,23 +459,11 @@ describe Projects::MergeRequestsController do format: format end - it_behaves_like "loads labels", :commits + it 'renders the commits template to a string' do + go format: 'json' - context 'as html' do - it 'renders the show template' do - go - - expect(response).to render_template('show') - end - end - - context 'as json' do - it 'renders the commits template to a string' do - go format: 'json' - - expect(response).to render_template('projects/merge_requests/show/_commits') - expect(json_response).to have_key('html') - end + expect(response).to render_template('projects/merge_requests/_commits') + expect(json_response).to have_key('html') end end @@ -759,106 +472,16 @@ describe Projects::MergeRequestsController do create(:ci_pipeline, project: merge_request.source_project, ref: merge_request.source_branch, sha: merge_request.diff_head_sha) - end - - context 'when using HTML format' do - it_behaves_like "loads labels", :pipelines - end - context 'when using JSON format' do - before do - get :pipelines, - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid, - format: :json - end - - it 'responds with serialized pipelines' do - expect(json_response).not_to be_empty - end - end - end - - 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) - - get :conflicts, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - format: 'json' - end - - it 'returns a 200 status code' do - expect(response).to have_http_status(:ok) - end - - it 'returns JSON with a message' do - expect(json_response.keys).to contain_exactly('message', 'type') - end + get :pipelines, + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid, + format: :json end - context 'with valid conflicts' do - before do - get :conflicts, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - format: 'json' - end - - it 'matches the schema' do - expect(response).to match_response_schema('conflicts') - end - - it 'includes meta info about the MR' do - expect(json_response['commit_message']).to include('Merge branch') - expect(json_response['commit_sha']).to match(/\h{40}/) - expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch) - expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch) - end - - it 'includes each file that has conflicts' do - filenames = json_response['files'].map { |file| file['new_path'] } - - expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb') - end - - it 'splits files into sections with lines' do - json_response['files'].each do |file| - file['sections'].each do |section| - expect(section).to include('conflict', 'lines') - - section['lines'].each do |line| - if section['conflict'] - expect(line['type']).to be_in(%w(old new)) - expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer)) - else - if line['type'].nil? - expect(line['old_line']).not_to eq(nil) - expect(line['new_line']).not_to eq(nil) - else - expect(line['type']).to eq('match') - expect(line['old_line']).to eq(nil) - expect(line['new_line']).to eq(nil) - end - end - end - end - end - end - - it 'has unique section IDs across files' do - section_ids = json_response['files'].flat_map do |file| - file['sections'].map { |section| section['id'] }.compact - end - - expect(section_ids.uniq).to eq(section_ids) - end + it 'responds with serialized pipelines' do + expect(json_response).not_to be_empty end end @@ -913,215 +536,6 @@ describe Projects::MergeRequestsController do end end - describe 'GET conflict_for_path' do - def conflict_for_path(path) - get :conflict_for_path, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - old_path: path, - new_path: path, - format: 'json' - end - - 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) - - conflict_for_path('files/ruby/regex.rb') - end - - it 'returns a 404 status code' do - expect(response).to have_http_status(:not_found) - end - end - - context 'when the file does not exist cannot be resolved in the UI' do - 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) - end - end - - context 'with an existing file' do - let(:path) { 'files/ruby/regex.rb' } - - 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 - - expect(json_response).to include('old_path' => path, - 'new_path' => path, - 'blob_icon' => 'file-text-o', - 'blob_path' => a_string_ending_with(path), - 'blob_ace_mode' => 'ruby', - 'content' => content) - end - end - end - - context 'POST resolve_conflicts' do - let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } - - def resolve_conflicts(files) - post :resolve_conflicts, - namespace_id: merge_request_with_conflicts.project.namespace.to_param, - project_id: merge_request_with_conflicts.project, - id: merge_request_with_conflicts.iid, - format: 'json', - files: files, - commit_message: 'Commit message' - end - - context 'with valid params' do - before do - resolved_files = [ - { - 'new_path' => 'files/ruby/popen.rb', - 'old_path' => 'files/ruby/popen.rb', - 'sections' => { - '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' - } - }, { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'creates a new commit on the branch' do - expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha) - expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message') - end - - it 'returns an OK response' do - expect(response).to have_http_status(:ok) - end - end - - context 'when sections are missing' do - before do - resolved_files = [ - { - 'new_path' => 'files/ruby/popen.rb', - 'old_path' => 'files/ruby/popen.rb', - 'sections' => { - '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' - } - }, { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'returns a 400 error' do - expect(response).to have_http_status(:bad_request) - end - - it 'has a message with the name of the first missing section' do - expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21') - end - - it 'does not create a new commit' do - expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) - end - end - - context 'when files are missing' do - before do - resolved_files = [ - { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'returns a 400 error' do - expect(response).to have_http_status(:bad_request) - end - - it 'has a message with the name of the missing file' do - expect(json_response['message']).to include('files/ruby/popen.rb') - end - - it 'does not create a new commit' do - expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) - end - end - - 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 - - resolved_files = [ - { - 'new_path' => 'files/ruby/popen.rb', - 'old_path' => 'files/ruby/popen.rb', - 'content' => content - }, { - 'new_path' => 'files/ruby/regex.rb', - 'old_path' => 'files/ruby/regex.rb', - 'sections' => { - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - } - } - ] - - resolve_conflicts(resolved_files) - end - - it 'returns a 400 error' do - expect(response).to have_http_status(:bad_request) - end - - it 'has a message with the path of the problem file' do - expect(json_response['message']).to include('files/ruby/popen.rb') - end - - it 'does not create a new commit' do - expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) - end - end - end - describe 'POST assign_related_issues' do let(:issue1) { create(:issue, project: project) } let(:issue2) { create(:issue, project: project) } diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index f8f95dd9bc8..a8c44d5c313 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -84,4 +84,57 @@ describe Projects::PipelineSchedulesController do end end end + + describe 'security' do + include AccessMatchersForController + + describe 'GET edit' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + + def go + get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + end + + describe 'GET take_ownership' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + + def go + post :take_ownership, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + end + + describe 'PUT update' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + + def go + put :update, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id, + schedule: { description: 'a' } + end + end + end end diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb index 5e6cd64c5c1..b88e801c3d7 100644 --- a/spec/features/abuse_report_spec.rb +++ b/spec/features/abuse_report_spec.rb @@ -12,7 +12,7 @@ feature 'Abuse reports', feature: true do click_link 'Report abuse' - fill_in 'abuse_report_message', with: 'This user send spam' + fill_in 'abuse_report_message', with: 'This user sends spam' click_button 'Send report' expect(page).to have_content 'Thank you for your report' diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 5b3323fed13..6ad2d456b93 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -9,31 +9,54 @@ describe "Admin Runners" do 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' + + 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' - 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_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 diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 69d5500848e..bb1fb5b3feb 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -1,18 +1,24 @@ 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 } - gitlab_sign_in(current_user) + before do + project.add_master(current_user) + sign_in(current_user) end - describe 'new merge request dropdown' do + 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 @@ -21,26 +27,87 @@ describe 'Dashboard Merge Requests' do 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 295262980a6..b0e4036f27c 100644 --- a/spec/features/dashboard/milestone_filter_spec.rb +++ b/spec/features/dashboard/milestone_filter_spec.rb @@ -1,10 +1,12 @@ 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 } @@ -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/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 2a8185ca669..f29186f368d 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -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/todos/target_state_spec.rb b/spec/features/dashboard/todos/target_state_spec.rb index 99b70b3d3a1..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 - gitlab_sign_in 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 032fb479076..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] - gitlab_sign_in(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 498bbac6d14..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) } @@ -18,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) @@ -32,41 +32,41 @@ describe "Dashboard > User sorts todos", feature: true do issue_2.labels << label_3 issue_1.labels << label_2 - gitlab_sign_in(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 @@ -88,12 +88,12 @@ describe "Dashboard > User sorts todos", feature: true do 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..7fa4d198e00 --- /dev/null +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -0,0 +1,338 @@ +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 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/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index ea749528c11..d492a15ea17 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -129,7 +129,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do before do large_diff.find('.diff-line-num', match: :prefer_exact).hover - large_diff.find('.add-diff-note').click + large_diff.find('.add-diff-note', match: :prefer_exact).click large_diff.find('.note-textarea').send_keys comment_text large_diff.find_button('Comment').click wait_for_requests diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index 5ad777248ec..56e163ec4d0 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -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 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 5af94e4069b..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) - gitlab_sign_in(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 4e4cf12e8af..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) } @@ -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 5d00ed30c83..a9a654b20e2 100644 --- a/spec/features/groups/members/list_spec.rb +++ b/spec/features/groups/members/manage_members.rb @@ -1,37 +1,16 @@ 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 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 - scenario 'update user to owner level', :js do group.add_owner(user1) group.add_developer(user2) @@ -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 135bb3572bc..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) - gitlab_sign_in(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 40f3b166e74..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) - gitlab_sign_in(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 3813308c237..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) } @@ -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 719fa0b40b8..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) } diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb new file mode 100644 index 00000000000..4d7a7dc1806 --- /dev/null +++ b/spec/features/issuables/user_sees_sidebar_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe 'Issue Sidebar on Mobile' do + include MobileHelpers + + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + before do + sign_in(user) + end + + context 'mobile sidebar on merge requests', js: true do + before do + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end + + context 'mobile sidebar on issues', js: true do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 863f8f75cd8..4cb728cc82b 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -459,7 +459,7 @@ describe 'Filter issues', js: true, feature: true do context 'issue label clicked' do before do - find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click + find('.issues-list .issue .issue-main-info .issuable-info a .label', text: multiple_words_label.title).click end it 'filters' do diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 163bc4bb32f..09724781a27 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -154,20 +154,6 @@ feature 'Issue Sidebar', feature: true do end end - context 'as a allowed mobile user', js: true do - before do - project.team << [user, :developer] - resize_screen_xs - visit_issue(project, issue) - end - - context 'mobile sidebar' do - it 'collapses the sidebar for small screens' do - expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') - end - end - end - context 'as a guest' do before do project.team << [user, :guest] diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 53b8ba5b0f7..a8055b21cee 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -143,29 +143,8 @@ 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') gitlab_sign_in_via('saml', user, 'my-uid') diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 8f7adbccaaa..6a08e50bf5e 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -65,7 +65,7 @@ feature 'Create New Merge Request', feature: true, js: true do it 'does not leak the private project name & namespace' do private_project = create(:project, :private) - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) expect(page).not_to have_content private_project.path_with_namespace expect(page).to have_content project.path_with_namespace @@ -76,7 +76,7 @@ feature 'Create New Merge Request', feature: true, js: true do it 'does not leak the private project name & namespace' do private_project = create(:project, :private) - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { source_project_id: private_project.id }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_project_id: private_project.id }) expect(page).not_to have_content private_project.path_with_namespace expect(page).to have_content project.path_with_namespace @@ -84,13 +84,13 @@ feature 'Create New Merge Request', feature: true, js: true do end it 'populates source branch button' do - visit new_namespace_project_merge_request_path(project.namespace, project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit namespace_project_new_merge_request_path(project.namespace, project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' }) expect(find('.js-source-branch')).to have_content('fix') end it 'allows to change the diff view' do - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' }) click_link 'Changes' @@ -106,7 +106,7 @@ feature 'Create New Merge Request', feature: true, js: true do end it 'does not allow non-existing branches' do - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) expect(page).to have_content('The form contains the following errors') expect(page).to have_content('Source branch "non-exist-source" does not exist') @@ -115,7 +115,7 @@ feature 'Create New Merge Request', feature: true, js: true do context 'when a branch contains commits that both delete and add the same image' do it 'renders the diff successfully' do - visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' }) + visit namespace_project_new_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' }) click_link "Changes" @@ -125,7 +125,7 @@ feature 'Create New Merge Request', feature: true, js: true do # Isolates a regression (see #24627) it 'does not show error messages on initial form' do - visit new_namespace_project_merge_request_path(project.namespace, project) + visit namespace_project_new_merge_request_path(project.namespace, project) expect(page).not_to have_selector('#error_explanation') expect(page).not_to have_content('The form contains the following error') end @@ -138,7 +138,7 @@ feature 'Create New Merge Request', feature: true, js: true do end it 'shows pipelines for a new merge request' do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' }) diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 1996c2fa09a..d03d498ce21 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -23,7 +23,7 @@ describe 'New/edit merge request', feature: true, js: true do context 'new merge request' do before do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { @@ -182,7 +182,7 @@ describe 'New/edit merge request', feature: true, js: true do context 'new merge request' do before do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( fork_project.namespace, fork_project, merge_request: { 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 71aa71e380e..a1f123f15ec 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -131,7 +131,7 @@ feature 'Merge Requests > User uses quick actions', feature: true, js: true do end it 'changes target_branch in new merge_request' do - visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) + visit namespace_project_new_merge_request_path(another_project.namespace, another_project, new_url_opts) fill_in "merge_request_title", with: 'My brand new feature' fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 3ac1f603de6..d8e9b949204 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -12,7 +12,7 @@ describe 'Merge request', :feature, :js do context 'new merge request' do before do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_requests/wip_message_spec.rb index 72d001bf408..0e304ba50af 100644 --- a/spec/features/merge_requests/wip_message_spec.rb +++ b/spec/features/merge_requests/wip_message_spec.rb @@ -11,7 +11,7 @@ feature 'Work In Progress help message', feature: true do context 'with WIP commits' do it 'shows a specific WIP hint' do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { @@ -32,7 +32,7 @@ feature 'Work In Progress help message', feature: true do context 'without WIP commits' do it 'shows the regular WIP message' do - visit new_namespace_project_merge_request_path( + visit namespace_project_new_merge_request_path( project.namespace, project, merge_request: { diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb index 2d36f3d020f..86c9df5ff86 100644 --- a/spec/features/profiles/password_spec.rb +++ b/spec/features/profiles/password_spec.rb @@ -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/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/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/branches_spec.rb b/spec/features/projects/branches_spec.rb index 8694366de35..0050864d305 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -21,10 +21,48 @@ describe 'Branches', feature: true do it 'shows all the branches' do visit namespace_project_branches_path(project.namespace, project) - repository.branches { |branch| expect(page).to have_content("#{branch.name}") } + repository.branches_sorted_by(:name).first(20).each do |branch| + expect(page).to have_content("#{branch.name}") + end expect(page).to have_content("Protected branches can be managed in project settings") end + it 'sorts the branches by name' do + visit namespace_project_branches_path(project.namespace, project) + + click_button "Name" # Open sorting dropdown + click_link "Name" + + sorted = repository.branches_sorted_by(:name).first(20).map do |branch| + Regexp.escape(branch.name) + end + expect(page).to have_content(/#{sorted.join(".*")}/) + end + + it 'sorts the branches by last updated' do + visit namespace_project_branches_path(project.namespace, project) + + click_button "Name" # Open sorting dropdown + click_link "Last updated" + + sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch| + Regexp.escape(branch.name) + end + expect(page).to have_content(/#{sorted.join(".*")}/) + end + + it 'sorts the branches by oldest updated' do + visit namespace_project_branches_path(project.namespace, project) + + click_button "Name" # Open sorting dropdown + click_link "Oldest updated" + + sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch| + Regexp.escape(branch.name) + end + expect(page).to have_content(/#{sorted.join(".*")}/) + end + it 'avoids a N+1 query in branches index' do control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb index b48dcf6c774..a98a69a0fd6 100644 --- a/spec/features/projects/environments/environment_metrics_spec.rb +++ b/spec/features/projects/environments/environment_metrics_spec.rb @@ -27,7 +27,7 @@ feature 'Environment > Metrics', :feature do scenario 'shows metrics' do click_link('See metrics') - expect(page).to have_css('svg.prometheus-graph') + expect(page).to have_css('div#prometheus-graphs') end end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 83883dba0ba..cf4d996a32d 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -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/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb index 2a82c3ac179..34aef958ec6 100644 --- a/spec/features/projects/files/browse_files_spec.rb +++ b/spec/features/projects/files/browse_files_spec.rb @@ -12,7 +12,7 @@ feature 'user browses project', feature: true, js: true do 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/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index a111aa87c52..3f8d2255298 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -98,6 +98,6 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end def project_hook_exists?(project) - Gitlab::Git::Hook.new('post-receive', project.repository.path).exists? + Gitlab::Git::Hook.new('post-receive', project).exists? end end diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 6de8855016d..58054bbbbed 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -23,7 +23,7 @@ feature 'Merge Request button', feature: true do end it 'shows Create merge request button' do - href = new_namespace_project_merge_request_path(project.namespace, + href = namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: 'feature', target_branch: 'master' }) @@ -67,7 +67,7 @@ feature 'Merge Request button', feature: true do let(:user) { forked_project.owner } it 'shows Create merge request button' do - href = new_namespace_project_merge_request_path(forked_project.namespace, + href = namespace_project_new_merge_request_path(forked_project.namespace, forked_project, merge_request: { source_branch: 'feature', target_branch: 'master' }) diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb index f2a2fd0311f..7ce3156215a 100644 --- a/spec/features/projects/merge_requests/list_spec.rb +++ b/spec/features/projects/merge_requests/list_spec.rb @@ -27,7 +27,7 @@ feature 'Merge Requests List' do it 'empty state should have a create merge request button' do visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_link 'New merge request', href: new_namespace_project_merge_request_path(project.namespace, project) + expect(page).to have_link 'New merge request', href: namespace_project_new_merge_request_path(project.namespace, project) end context 'if there are merge requests' 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/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb index 2ea50e8f672..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 @@ -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/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb index f375e1215db..5d0acad3832 100644 --- a/spec/features/projects/user_create_dir_spec.rb +++ b/spec/features/projects/user_create_dir_spec.rb @@ -51,7 +51,7 @@ feature 'New directory creation', feature: true, js: true do expect(page).to have_content 'New Merge Request' expect(page).to have_content "From #{new_branch_name} into master" expect(page).to have_content 'Add new directory' - expect(current_path).to eq(new_namespace_project_merge_request_path(project.namespace, project)) + expect(current_path).to eq(namespace_project_new_merge_request_path(project.namespace, project)) end end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index f33406a40a7..5e26b8bbed6 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -239,7 +239,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/merge_requests/new" do - subject { new_namespace_project_merge_request_path(project.namespace, project) } + subject { namespace_project_new_merge_request_path(project.namespace, project) } 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 16a1331b2f3..59655b0c31a 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -452,7 +452,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/merge_requests/new" do - subject { new_namespace_project_merge_request_path(project.namespace, project) } + subject { namespace_project_new_merge_request_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index d310e7501ec..c7e2e3d8a34 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -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 diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb deleted file mode 100644 index 41b32bdedc3..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 - gitlab_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) - gitlab_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 - gitlab_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) - gitlab_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, 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) - - 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) - - gitlab_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, 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) - gitlab_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 - gitlab_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 -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/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 84a60ce13fc..8da22dc78fa 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -84,7 +84,7 @@ describe GroupsHelper do end end - describe 'group_title' do + 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) } diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 9a4086725d2..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) } diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index cb727430117..9e561d0f191 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -170,6 +170,11 @@ describe SubmoduleHelper do expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) end + it 'with trailing whitespace' do + result = relative_self_links('../test.git ', commit_id) + expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) + end + it 'two levels down' do result = relative_self_links('../../test.git', commit_id) expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"]) 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/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/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index f7708301b6e..0132f4b7c93 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -66,4 +66,38 @@ describe('Dropdown User', () => { window.gon = {}; }); }); + + describe('hideCurrentUser', () => { + const fixtureTemplate = 'issues/issue_list.html.raw'; + preloadFixtures(fixtureTemplate); + + let dropdown; + let authorFilterDropdownElement; + + beforeEach(() => { + loadFixtures(fixtureTemplate); + authorFilterDropdownElement = document.querySelector('#js-dropdown-author'); + const dummyInput = document.createElement('div'); + dropdown = new gl.DropdownUser(null, authorFilterDropdownElement, dummyInput); + }); + + const findCurrentUserElement = () => authorFilterDropdownElement.querySelector('.js-current-user'); + + it('hides the current user from dropdown', () => { + const currentUserElement = findCurrentUserElement(); + expect(currentUserElement).not.toBe(null); + + dropdown.hideCurrentUser(); + + expect(currentUserElement.classList).toContain('hidden'); + }); + + it('does nothing if no user is logged in', () => { + const currentUserElement = findCurrentUserElement(); + currentUserElement.parentNode.removeChild(currentUserElement); + expect(findCurrentUserElement()).toBe(null); + + dropdown.hideCurrentUser(); + }); + }); }); diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index daaddd8f390..7e2f364ffa4 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -55,27 +55,14 @@ 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, action: :show, format: :html, view: 'inline') - get action, + def render_merge_request(fixture_file_name, merge_request) + get :show, namespace_id: project.namespace.to_param, project_id: project, id: merge_request.to_param, - format: format, - view: view + format: :html expect(response).to be_success store_frontend_fixture(response, fixture_file_name) diff --git a/spec/javascripts/fixtures/merge_requests_diffs.rb b/spec/javascripts/fixtures/merge_requests_diffs.rb new file mode 100644 index 00000000000..ac5b06ace6d --- /dev/null +++ b/spec/javascripts/fixtures/merge_requests_diffs.rb @@ -0,0 +1,57 @@ + +require 'spec_helper' + +describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:path) { "files/ruby/popen.rb" } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end + + render_views + + before(:all) do + clean_frontend_fixtures('merge_request_diffs/') + end + + before(:each) do + sign_in(admin) + end + + it 'merge_request_diffs/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) + end + + it 'merge_request_diffs/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, view: 'parallel') + end + + private + + def render_merge_request(fixture_file_name, merge_request, view: 'inline') + get :show, + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.to_param, + format: :json, + view: view + + expect(response).to be_success + store_frontend_fixture(response, fixture_file_name) + end +end 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/lib/utils/dom_utils_spec.js b/spec/javascripts/lib/utils/dom_utils_spec.js new file mode 100644 index 00000000000..867bf5912d1 --- /dev/null +++ b/spec/javascripts/lib/utils/dom_utils_spec.js @@ -0,0 +1,35 @@ +import { addClassIfElementExists } from '~/lib/utils/dom_utils'; + +describe('DOM Utils', () => { + describe('addClassIfElementExists', () => { + const className = 'biology'; + const fixture = ` + <div class="parent"> + <div class="child"></div> + </div> + `; + + let parentElement; + + beforeEach(() => { + setFixtures(fixture); + parentElement = document.querySelector('.parent'); + }); + + it('adds class if element exists', () => { + const childElement = parentElement.querySelector('.child'); + expect(childElement).not.toBe(null); + + addClassIfElementExists(childElement, className); + + expect(childElement.classList).toContain(className); + }); + + it('does not throw if element does not exist', () => { + const childElement = parentElement.querySelector('.other-child'); + expect(childElement).toBe(null); + + addClassIfElementExists(childElement, className); + }); + }); +}); diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index 9e9eb17d439..395dc560671 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -15,7 +15,7 @@ describe('Merge request notes', () => { gl.utils = gl.utils || {}; const discussionTabFixture = 'merge_requests/diff_comment.html.raw'; - const changesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; + const changesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; preloadFixtures(discussionTabFixture, changesTabJsonFixture); describe('Discussion tab with diff comments', () => { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index bb6b5d852d3..49ef21f75de 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -23,8 +23,8 @@ import 'vendor/jquery.scrollTo'; $.extend(stubLocation, defaults, stubs || {}); }; - const inlineChangesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; - const parallelChangesTabJsonFixture = 'merge_requests/parallel_changes_tab_with_comments.json'; + const inlineChangesTabJsonFixture = 'merge_request_diffs/inline_changes_tab_with_comments.json'; + const parallelChangesTabJsonFixture = 'merge_request_diffs/parallel_changes_tab_with_comments.json'; preloadFixtures( 'merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw', @@ -52,14 +52,10 @@ import 'vendor/jquery.scrollTo'; loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.subject = this.class.activateTab; }); - it('shows the first tab when action is show', function () { + it('shows the notes tab when action is show', function () { this.subject('show'); expect($('#notes')).toHaveClass('active'); }); - it('shows the notes tab when action is notes', function () { - this.subject('notes'); - expect($('#notes')).toHaveClass('active'); - }); it('shows the commits tab when action is commits', function () { this.subject('commits'); expect($('#commits')).toHaveClass('active'); @@ -161,7 +157,7 @@ import 'vendor/jquery.scrollTo'; setLocation({ pathname: '/foo/bar/merge_requests/1/commits' }); - expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); }); @@ -170,7 +166,7 @@ import 'vendor/jquery.scrollTo'; pathname: '/foo/bar/merge_requests/1/diffs' }); - expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); @@ -178,7 +174,7 @@ import 'vendor/jquery.scrollTo'; setLocation({ pathname: '/foo/bar/merge_requests/1/diffs.html' }); - expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); }); diff --git a/spec/javascripts/monitoring/deployments_spec.js b/spec/javascripts/monitoring/deployments_spec.js deleted file mode 100644 index 19bc11d0f24..00000000000 --- a/spec/javascripts/monitoring/deployments_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import d3 from 'd3'; -import PrometheusGraph from '~/monitoring/prometheus_graph'; -import Deployments from '~/monitoring/deployments'; -import { prometheusMockData } from './prometheus_mock_data'; - -describe('Metrics deployments', () => { - const fixtureName = 'environments/metrics/metrics.html.raw'; - let deployment; - let prometheusGraph; - - const graphElement = () => document.querySelector('.prometheus-graph'); - - preloadFixtures(fixtureName); - - beforeEach((done) => { - // Setup the view - loadFixtures(fixtureName); - - d3.selectAll('.prometheus-graph') - .append('g') - .attr('class', 'graph-container'); - - prometheusGraph = new PrometheusGraph(); - deployment = new Deployments(1000, 500); - - spyOn(prometheusGraph, 'init'); - spyOn($, 'ajax').and.callFake(() => { - const d = $.Deferred(); - d.resolve({ - deployments: [{ - id: 1, - created_at: deployment.chartData[10].time, - sha: 'testing', - tag: false, - ref: { - name: 'testing', - }, - }, { - id: 2, - created_at: deployment.chartData[15].time, - sha: '', - tag: true, - ref: { - name: 'tag', - }, - }], - }); - - setTimeout(done); - - return d.promise(); - }); - - prometheusGraph.configureGraph(); - prometheusGraph.transformData(prometheusMockData.metrics); - - deployment.init(prometheusGraph.graphSpecificProperties.memory_values.data); - }); - - it('creates line on graph for deploment', () => { - expect( - graphElement().querySelectorAll('.deployment-line').length, - ).toBe(2); - }); - - it('creates hidden deploy boxes', () => { - expect( - graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box').length, - ).toBe(2); - }); - - it('hides the info boxes by default', () => { - expect( - graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length, - ).toBe(2); - }); - - it('shows sha short code when tag is false', () => { - expect( - graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box').textContent.trim(), - ).toContain('testin'); - }); - - it('shows ref name when tag is true', () => { - expect( - graphElement().querySelector('.deploy-info-2-cpu_values .js-deploy-info-box').textContent.trim(), - ).toContain('tag'); - }); - - it('shows info box when moving mouse over line', () => { - deployment.mouseOverDeployInfo(deployment.data[0].xPos, 'cpu_values'); - - expect( - graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length, - ).toBe(1); - - expect( - graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'), - ).toBeNull(); - }); - - it('hides previously visible info box when moving mouse away', () => { - deployment.mouseOverDeployInfo(500, 'cpu_values'); - - expect( - graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length, - ).toBe(2); - - expect( - graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'), - ).not.toBeNull(); - }); - - describe('refText', () => { - it('returns shortened SHA', () => { - expect( - Deployments.refText({ - tag: false, - sha: '123456789', - }), - ).toBe('123456'); - }); - - it('returns tag name', () => { - expect( - Deployments.refText({ - tag: true, - ref: 'v1.0', - }), - ).toBe('v1.0'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js new file mode 100644 index 00000000000..6f4cb989847 --- /dev/null +++ b/spec/javascripts/monitoring/mock_data.js @@ -0,0 +1,4229 @@ +/* eslint-disable quote-props, indent, comma-dangle */ + +const metricsGroupsAPIResponse = { + 'success': true, + 'data': [ + { + 'group': 'Kubernetes', + 'priority': 1, + 'metrics': [ + { + 'title': 'Memory usage', + 'weight': 1, + 'queries': [ + { + 'query_range': 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', + 'label': 'Container memory', + 'unit': 'MiB', + 'result': [ + { + 'metric': {}, + 'values': [ + [ + 1495700554.925, + '8.0390625' + ], + [ + 1495700614.925, + '8.0390625' + ], + [ + 1495700674.925, + '8.0390625' + ], + [ + 1495700734.925, + '8.0390625' + ], + [ + 1495700794.925, + '8.0390625' + ], + [ + 1495700854.925, + '8.0390625' + ], + [ + 1495700914.925, + '8.0390625' + ], + [ + 1495700974.925, + '8.0390625' + ], + [ + 1495701034.925, + '8.0390625' + ], + [ + 1495701094.925, + '8.0390625' + ], + [ + 1495701154.925, + '8.0390625' + ], + [ + 1495701214.925, + '8.0390625' + ], + [ + 1495701274.925, + '8.0390625' + ], + [ + 1495701334.925, + '8.0390625' + ], + [ + 1495701394.925, + '8.0390625' + ], + [ + 1495701454.925, + '8.0390625' + ], + [ + 1495701514.925, + '8.0390625' + ], + [ + 1495701574.925, + '8.0390625' + ], + [ + 1495701634.925, + '8.0390625' + ], + [ + 1495701694.925, + '8.0390625' + ], + [ + 1495701754.925, + '8.0390625' + ], + [ + 1495701814.925, + '8.0390625' + ], + [ + 1495701874.925, + '8.0390625' + ], + [ + 1495701934.925, + '8.0390625' + ], + [ + 1495701994.925, + '8.0390625' + ], + [ + 1495702054.925, + '8.0390625' + ], + [ + 1495702114.925, + '8.0390625' + ], + [ + 1495702174.925, + '8.0390625' + ], + [ + 1495702234.925, + '8.0390625' + ], + [ + 1495702294.925, + '8.0390625' + ], + [ + 1495702354.925, + '8.0390625' + ], + [ + 1495702414.925, + '8.0390625' + ], + [ + 1495702474.925, + '8.0390625' + ], + [ + 1495702534.925, + '8.0390625' + ], + [ + 1495702594.925, + '8.0390625' + ], + [ + 1495702654.925, + '8.0390625' + ], + [ + 1495702714.925, + '8.0390625' + ], + [ + 1495702774.925, + '8.0390625' + ], + [ + 1495702834.925, + '8.0390625' + ], + [ + 1495702894.925, + '8.0390625' + ], + [ + 1495702954.925, + '8.0390625' + ], + [ + 1495703014.925, + '8.0390625' + ], + [ + 1495703074.925, + '8.0390625' + ], + [ + 1495703134.925, + '8.0390625' + ], + [ + 1495703194.925, + '8.0390625' + ], + [ + 1495703254.925, + '8.03515625' + ], + [ + 1495703314.925, + '8.03515625' + ], + [ + 1495703374.925, + '8.03515625' + ], + [ + 1495703434.925, + '8.03515625' + ], + [ + 1495703494.925, + '8.03515625' + ], + [ + 1495703554.925, + '8.03515625' + ], + [ + 1495703614.925, + '8.03515625' + ], + [ + 1495703674.925, + '8.03515625' + ], + [ + 1495703734.925, + '8.03515625' + ], + [ + 1495703794.925, + '8.03515625' + ], + [ + 1495703854.925, + '8.03515625' + ], + [ + 1495703914.925, + '8.03515625' + ], + [ + 1495703974.925, + '8.03515625' + ], + [ + 1495704034.925, + '8.03515625' + ], + [ + 1495704094.925, + '8.03515625' + ], + [ + 1495704154.925, + '8.03515625' + ], + [ + 1495704214.925, + '7.9296875' + ], + [ + 1495704274.925, + '7.9296875' + ], + [ + 1495704334.925, + '7.9296875' + ], + [ + 1495704394.925, + '7.9296875' + ], + [ + 1495704454.925, + '7.9296875' + ], + [ + 1495704514.925, + '7.9296875' + ], + [ + 1495704574.925, + '7.9296875' + ], + [ + 1495704634.925, + '7.9296875' + ], + [ + 1495704694.925, + '7.9296875' + ], + [ + 1495704754.925, + '7.9296875' + ], + [ + 1495704814.925, + '7.9296875' + ], + [ + 1495704874.925, + '7.9296875' + ], + [ + 1495704934.925, + '7.9296875' + ], + [ + 1495704994.925, + '7.9296875' + ], + [ + 1495705054.925, + '7.9296875' + ], + [ + 1495705114.925, + '7.9296875' + ], + [ + 1495705174.925, + '7.9296875' + ], + [ + 1495705234.925, + '7.9296875' + ], + [ + 1495705294.925, + '7.9296875' + ], + [ + 1495705354.925, + '7.9296875' + ], + [ + 1495705414.925, + '7.9296875' + ], + [ + 1495705474.925, + '7.9296875' + ], + [ + 1495705534.925, + '7.9296875' + ], + [ + 1495705594.925, + '7.9296875' + ], + [ + 1495705654.925, + '7.9296875' + ], + [ + 1495705714.925, + '7.9296875' + ], + [ + 1495705774.925, + '7.9296875' + ], + [ + 1495705834.925, + '7.9296875' + ], + [ + 1495705894.925, + '7.9296875' + ], + [ + 1495705954.925, + '7.9296875' + ], + [ + 1495706014.925, + '7.9296875' + ], + [ + 1495706074.925, + '7.9296875' + ], + [ + 1495706134.925, + '7.9296875' + ], + [ + 1495706194.925, + '7.9296875' + ], + [ + 1495706254.925, + '7.9296875' + ], + [ + 1495706314.925, + '7.9296875' + ], + [ + 1495706374.925, + '7.9296875' + ], + [ + 1495706434.925, + '7.9296875' + ], + [ + 1495706494.925, + '7.9296875' + ], + [ + 1495706554.925, + '7.9296875' + ], + [ + 1495706614.925, + '7.9296875' + ], + [ + 1495706674.925, + '7.9296875' + ], + [ + 1495706734.925, + '7.9296875' + ], + [ + 1495706794.925, + '7.9296875' + ], + [ + 1495706854.925, + '7.9296875' + ], + [ + 1495706914.925, + '7.9296875' + ], + [ + 1495706974.925, + '7.9296875' + ], + [ + 1495707034.925, + '7.9296875' + ], + [ + 1495707094.925, + '7.9296875' + ], + [ + 1495707154.925, + '7.9296875' + ], + [ + 1495707214.925, + '7.9296875' + ], + [ + 1495707274.925, + '7.9296875' + ], + [ + 1495707334.925, + '7.9296875' + ], + [ + 1495707394.925, + '7.9296875' + ], + [ + 1495707454.925, + '7.9296875' + ], + [ + 1495707514.925, + '7.9296875' + ], + [ + 1495707574.925, + '7.9296875' + ], + [ + 1495707634.925, + '7.9296875' + ], + [ + 1495707694.925, + '7.9296875' + ], + [ + 1495707754.925, + '7.9296875' + ], + [ + 1495707814.925, + '7.9296875' + ], + [ + 1495707874.925, + '7.9296875' + ], + [ + 1495707934.925, + '7.9296875' + ], + [ + 1495707994.925, + '7.9296875' + ], + [ + 1495708054.925, + '7.9296875' + ], + [ + 1495708114.925, + '7.9296875' + ], + [ + 1495708174.925, + '7.9296875' + ], + [ + 1495708234.925, + '7.9296875' + ], + [ + 1495708294.925, + '7.9296875' + ], + [ + 1495708354.925, + '7.9296875' + ], + [ + 1495708414.925, + '7.9296875' + ], + [ + 1495708474.925, + '7.9296875' + ], + [ + 1495708534.925, + '7.9296875' + ], + [ + 1495708594.925, + '7.9296875' + ], + [ + 1495708654.925, + '7.9296875' + ], + [ + 1495708714.925, + '7.9296875' + ], + [ + 1495708774.925, + '7.9296875' + ], + [ + 1495708834.925, + '7.9296875' + ], + [ + 1495708894.925, + '7.9296875' + ], + [ + 1495708954.925, + '7.8984375' + ], + [ + 1495709014.925, + '7.8984375' + ], + [ + 1495709074.925, + '7.8984375' + ], + [ + 1495709134.925, + '7.8984375' + ], + [ + 1495709194.925, + '7.8984375' + ], + [ + 1495709254.925, + '7.89453125' + ], + [ + 1495709314.925, + '7.89453125' + ], + [ + 1495709374.925, + '7.89453125' + ], + [ + 1495709434.925, + '7.89453125' + ], + [ + 1495709494.925, + '7.89453125' + ], + [ + 1495709554.925, + '7.89453125' + ], + [ + 1495709614.925, + '7.89453125' + ], + [ + 1495709674.925, + '7.89453125' + ], + [ + 1495709734.925, + '7.89453125' + ], + [ + 1495709794.925, + '7.89453125' + ], + [ + 1495709854.925, + '7.89453125' + ], + [ + 1495709914.925, + '7.89453125' + ], + [ + 1495709974.925, + '7.89453125' + ], + [ + 1495710034.925, + '7.89453125' + ], + [ + 1495710094.925, + '7.89453125' + ], + [ + 1495710154.925, + '7.89453125' + ], + [ + 1495710214.925, + '7.89453125' + ], + [ + 1495710274.925, + '7.89453125' + ], + [ + 1495710334.925, + '7.89453125' + ], + [ + 1495710394.925, + '7.89453125' + ], + [ + 1495710454.925, + '7.89453125' + ], + [ + 1495710514.925, + '7.89453125' + ], + [ + 1495710574.925, + '7.89453125' + ], + [ + 1495710634.925, + '7.89453125' + ], + [ + 1495710694.925, + '7.89453125' + ], + [ + 1495710754.925, + '7.89453125' + ], + [ + 1495710814.925, + '7.89453125' + ], + [ + 1495710874.925, + '7.89453125' + ], + [ + 1495710934.925, + '7.89453125' + ], + [ + 1495710994.925, + '7.89453125' + ], + [ + 1495711054.925, + '7.89453125' + ], + [ + 1495711114.925, + '7.89453125' + ], + [ + 1495711174.925, + '7.8515625' + ], + [ + 1495711234.925, + '7.8515625' + ], + [ + 1495711294.925, + '7.8515625' + ], + [ + 1495711354.925, + '7.8515625' + ], + [ + 1495711414.925, + '7.8515625' + ], + [ + 1495711474.925, + '7.8515625' + ], + [ + 1495711534.925, + '7.8515625' + ], + [ + 1495711594.925, + '7.8515625' + ], + [ + 1495711654.925, + '7.8515625' + ], + [ + 1495711714.925, + '7.8515625' + ], + [ + 1495711774.925, + '7.8515625' + ], + [ + 1495711834.925, + '7.8515625' + ], + [ + 1495711894.925, + '7.8515625' + ], + [ + 1495711954.925, + '7.8515625' + ], + [ + 1495712014.925, + '7.8515625' + ], + [ + 1495712074.925, + '7.8515625' + ], + [ + 1495712134.925, + '7.8515625' + ], + [ + 1495712194.925, + '7.8515625' + ], + [ + 1495712254.925, + '7.8515625' + ], + [ + 1495712314.925, + '7.8515625' + ], + [ + 1495712374.925, + '7.8515625' + ], + [ + 1495712434.925, + '7.83203125' + ], + [ + 1495712494.925, + '7.83203125' + ], + [ + 1495712554.925, + '7.83203125' + ], + [ + 1495712614.925, + '7.83203125' + ], + [ + 1495712674.925, + '7.83203125' + ], + [ + 1495712734.925, + '7.83203125' + ], + [ + 1495712794.925, + '7.83203125' + ], + [ + 1495712854.925, + '7.83203125' + ], + [ + 1495712914.925, + '7.83203125' + ], + [ + 1495712974.925, + '7.83203125' + ], + [ + 1495713034.925, + '7.83203125' + ], + [ + 1495713094.925, + '7.83203125' + ], + [ + 1495713154.925, + '7.83203125' + ], + [ + 1495713214.925, + '7.83203125' + ], + [ + 1495713274.925, + '7.83203125' + ], + [ + 1495713334.925, + '7.83203125' + ], + [ + 1495713394.925, + '7.8125' + ], + [ + 1495713454.925, + '7.8125' + ], + [ + 1495713514.925, + '7.8125' + ], + [ + 1495713574.925, + '7.8125' + ], + [ + 1495713634.925, + '7.8125' + ], + [ + 1495713694.925, + '7.8125' + ], + [ + 1495713754.925, + '7.8125' + ], + [ + 1495713814.925, + '7.8125' + ], + [ + 1495713874.925, + '7.8125' + ], + [ + 1495713934.925, + '7.8125' + ], + [ + 1495713994.925, + '7.8125' + ], + [ + 1495714054.925, + '7.8125' + ], + [ + 1495714114.925, + '7.8125' + ], + [ + 1495714174.925, + '7.8125' + ], + [ + 1495714234.925, + '7.8125' + ], + [ + 1495714294.925, + '7.8125' + ], + [ + 1495714354.925, + '7.80859375' + ], + [ + 1495714414.925, + '7.80859375' + ], + [ + 1495714474.925, + '7.80859375' + ], + [ + 1495714534.925, + '7.80859375' + ], + [ + 1495714594.925, + '7.80859375' + ], + [ + 1495714654.925, + '7.80859375' + ], + [ + 1495714714.925, + '7.80859375' + ], + [ + 1495714774.925, + '7.80859375' + ], + [ + 1495714834.925, + '7.80859375' + ], + [ + 1495714894.925, + '7.80859375' + ], + [ + 1495714954.925, + '7.80859375' + ], + [ + 1495715014.925, + '7.80859375' + ], + [ + 1495715074.925, + '7.80859375' + ], + [ + 1495715134.925, + '7.80859375' + ], + [ + 1495715194.925, + '7.80859375' + ], + [ + 1495715254.925, + '7.80859375' + ], + [ + 1495715314.925, + '7.80859375' + ], + [ + 1495715374.925, + '7.80859375' + ], + [ + 1495715434.925, + '7.80859375' + ], + [ + 1495715494.925, + '7.80859375' + ], + [ + 1495715554.925, + '7.80859375' + ], + [ + 1495715614.925, + '7.80859375' + ], + [ + 1495715674.925, + '7.80859375' + ], + [ + 1495715734.925, + '7.80859375' + ], + [ + 1495715794.925, + '7.80859375' + ], + [ + 1495715854.925, + '7.80859375' + ], + [ + 1495715914.925, + '7.80078125' + ], + [ + 1495715974.925, + '7.80078125' + ], + [ + 1495716034.925, + '7.80078125' + ], + [ + 1495716094.925, + '7.80078125' + ], + [ + 1495716154.925, + '7.80078125' + ], + [ + 1495716214.925, + '7.796875' + ], + [ + 1495716274.925, + '7.796875' + ], + [ + 1495716334.925, + '7.796875' + ], + [ + 1495716394.925, + '7.796875' + ], + [ + 1495716454.925, + '7.796875' + ], + [ + 1495716514.925, + '7.796875' + ], + [ + 1495716574.925, + '7.796875' + ], + [ + 1495716634.925, + '7.796875' + ], + [ + 1495716694.925, + '7.796875' + ], + [ + 1495716754.925, + '7.796875' + ], + [ + 1495716814.925, + '7.796875' + ], + [ + 1495716874.925, + '7.79296875' + ], + [ + 1495716934.925, + '7.79296875' + ], + [ + 1495716994.925, + '7.79296875' + ], + [ + 1495717054.925, + '7.79296875' + ], + [ + 1495717114.925, + '7.79296875' + ], + [ + 1495717174.925, + '7.7890625' + ], + [ + 1495717234.925, + '7.7890625' + ], + [ + 1495717294.925, + '7.7890625' + ], + [ + 1495717354.925, + '7.7890625' + ], + [ + 1495717414.925, + '7.7890625' + ], + [ + 1495717474.925, + '7.7890625' + ], + [ + 1495717534.925, + '7.7890625' + ], + [ + 1495717594.925, + '7.7890625' + ], + [ + 1495717654.925, + '7.7890625' + ], + [ + 1495717714.925, + '7.7890625' + ], + [ + 1495717774.925, + '7.7890625' + ], + [ + 1495717834.925, + '7.77734375' + ], + [ + 1495717894.925, + '7.77734375' + ], + [ + 1495717954.925, + '7.77734375' + ], + [ + 1495718014.925, + '7.77734375' + ], + [ + 1495718074.925, + '7.77734375' + ], + [ + 1495718134.925, + '7.7421875' + ], + [ + 1495718194.925, + '7.7421875' + ], + [ + 1495718254.925, + '7.7421875' + ], + [ + 1495718314.925, + '7.7421875' + ] + ] + } + ] + } + ] + }, + { + 'title': 'CPU usage', + 'weight': 1, + 'queries': [ + { + 'query_range': 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', + 'result': [ + { + 'metric': {}, + 'values': [ + [ + 1495700554.925, + '0.0010794445585559514' + ], + [ + 1495700614.925, + '0.003927214935433527' + ], + [ + 1495700674.925, + '0.0053045219047619975' + ], + [ + 1495700734.925, + '0.0048892095238097155' + ], + [ + 1495700794.925, + '0.005827140952381137' + ], + [ + 1495700854.925, + '0.00569846906219937' + ], + [ + 1495700914.925, + '0.004972616802849382' + ], + [ + 1495700974.925, + '0.005117509523809902' + ], + [ + 1495701034.925, + '0.00512389061919564' + ], + [ + 1495701094.925, + '0.005199100501890691' + ], + [ + 1495701154.925, + '0.005415746394885837' + ], + [ + 1495701214.925, + '0.005607682788146286' + ], + [ + 1495701274.925, + '0.005641300000000118' + ], + [ + 1495701334.925, + '0.0071166279368766495' + ], + [ + 1495701394.925, + '0.0063242138095234044' + ], + [ + 1495701454.925, + '0.005793314698235304' + ], + [ + 1495701514.925, + '0.00703934942237556' + ], + [ + 1495701574.925, + '0.006357007076123191' + ], + [ + 1495701634.925, + '0.003753167300126738' + ], + [ + 1495701694.925, + '0.005018469678430698' + ], + [ + 1495701754.925, + '0.0045217153371887' + ], + [ + 1495701814.925, + '0.006140104285714119' + ], + [ + 1495701874.925, + '0.004818684285714102' + ], + [ + 1495701934.925, + '0.005079509718955242' + ], + [ + 1495701994.925, + '0.005059981142498263' + ], + [ + 1495702054.925, + '0.005269098389538773' + ], + [ + 1495702114.925, + '0.005269954285714175' + ], + [ + 1495702174.925, + '0.014199241435795856' + ], + [ + 1495702234.925, + '0.01511936843111017' + ], + [ + 1495702294.925, + '0.0060933692920682875' + ], + [ + 1495702354.925, + '0.004945682380952493' + ], + [ + 1495702414.925, + '0.005641266666666565' + ], + [ + 1495702474.925, + '0.005223752857142996' + ], + [ + 1495702534.925, + '0.005743098505699831' + ], + [ + 1495702594.925, + '0.00538493380952391' + ], + [ + 1495702654.925, + '0.005507793883751339' + ], + [ + 1495702714.925, + '0.005666705714285466' + ], + [ + 1495702774.925, + '0.006231530000000112' + ], + [ + 1495702834.925, + '0.006570768635394899' + ], + [ + 1495702894.925, + '0.005551146666666895' + ], + [ + 1495702954.925, + '0.005602604737098058' + ], + [ + 1495703014.925, + '0.00613993580402159' + ], + [ + 1495703074.925, + '0.004770258764368832' + ], + [ + 1495703134.925, + '0.005512376671364914' + ], + [ + 1495703194.925, + '0.005254436666666674' + ], + [ + 1495703254.925, + '0.0050109839141320505' + ], + [ + 1495703314.925, + '0.0049478019256960016' + ], + [ + 1495703374.925, + '0.0037666860965123463' + ], + [ + 1495703434.925, + '0.004813526061656314' + ], + [ + 1495703494.925, + '0.005047748095238278' + ], + [ + 1495703554.925, + '0.00386494081008772' + ], + [ + 1495703614.925, + '0.004304037408111405' + ], + [ + 1495703674.925, + '0.004999466661587168' + ], + [ + 1495703734.925, + '0.004689140476190834' + ], + [ + 1495703794.925, + '0.004746126153582475' + ], + [ + 1495703854.925, + '0.004482706382572302' + ], + [ + 1495703914.925, + '0.004032808931864524' + ], + [ + 1495703974.925, + '0.005728319047618988' + ], + [ + 1495704034.925, + '0.004436139179627006' + ], + [ + 1495704094.925, + '0.004553455714285617' + ], + [ + 1495704154.925, + '0.003455244285714341' + ], + [ + 1495704214.925, + '0.004742244761904621' + ], + [ + 1495704274.925, + '0.005366978571428422' + ], + [ + 1495704334.925, + '0.004257954837665058' + ], + [ + 1495704394.925, + '0.005431603259831257' + ], + [ + 1495704454.925, + '0.0052009214498621986' + ], + [ + 1495704514.925, + '0.004317201904761618' + ], + [ + 1495704574.925, + '0.004307384285714157' + ], + [ + 1495704634.925, + '0.004789801146644822' + ], + [ + 1495704694.925, + '0.0051429795906706485' + ], + [ + 1495704754.925, + '0.005322495714285479' + ], + [ + 1495704814.925, + '0.004512809333244233' + ], + [ + 1495704874.925, + '0.004953843582568726' + ], + [ + 1495704934.925, + '0.005812690120858119' + ], + [ + 1495704994.925, + '0.004997024285714838' + ], + [ + 1495705054.925, + '0.005246216154439592' + ], + [ + 1495705114.925, + '0.0063494966618726795' + ], + [ + 1495705174.925, + '0.005306004342898225' + ], + [ + 1495705234.925, + '0.005081412857142978' + ], + [ + 1495705294.925, + '0.00511409523809522' + ], + [ + 1495705354.925, + '0.0047861001481192' + ], + [ + 1495705414.925, + '0.005107688228042962' + ], + [ + 1495705474.925, + '0.005271929582294012' + ], + [ + 1495705534.925, + '0.004453254502681249' + ], + [ + 1495705594.925, + '0.005799134293959226' + ], + [ + 1495705654.925, + '0.005340865929502478' + ], + [ + 1495705714.925, + '0.004911654761904942' + ], + [ + 1495705774.925, + '0.005888234873953261' + ], + [ + 1495705834.925, + '0.005565283333332954' + ], + [ + 1495705894.925, + '0.005522869047618869' + ], + [ + 1495705954.925, + '0.005177549737621646' + ], + [ + 1495706014.925, + '0.0053145810232096465' + ], + [ + 1495706074.925, + '0.004751095238095275' + ], + [ + 1495706134.925, + '0.006242077142856976' + ], + [ + 1495706194.925, + '0.00621034406957871' + ], + [ + 1495706254.925, + '0.006887592738978596' + ], + [ + 1495706314.925, + '0.006328128779726213' + ], + [ + 1495706374.925, + '0.007488363809523927' + ], + [ + 1495706434.925, + '0.006193758571428157' + ], + [ + 1495706494.925, + '0.0068798371839706935' + ], + [ + 1495706554.925, + '0.005757034340423128' + ], + [ + 1495706614.925, + '0.004571388497294698' + ], + [ + 1495706674.925, + '0.00620283044923395' + ], + [ + 1495706734.925, + '0.005607562380952455' + ], + [ + 1495706794.925, + '0.005506969933620308' + ], + [ + 1495706854.925, + '0.005621118095238131' + ], + [ + 1495706914.925, + '0.004876606098698849' + ], + [ + 1495706974.925, + '0.0047871205988517206' + ], + [ + 1495707034.925, + '0.00526405939458784' + ], + [ + 1495707094.925, + '0.005716323800605852' + ], + [ + 1495707154.925, + '0.005301459523809575' + ], + [ + 1495707214.925, + '0.0051613042857144905' + ], + [ + 1495707274.925, + '0.005384792857142714' + ], + [ + 1495707334.925, + '0.005259719047619222' + ], + [ + 1495707394.925, + '0.00584101142857182' + ], + [ + 1495707454.925, + '0.0060066121920326326' + ], + [ + 1495707514.925, + '0.006359978571428453' + ], + [ + 1495707574.925, + '0.006315876322151109' + ], + [ + 1495707634.925, + '0.005590012517198831' + ], + [ + 1495707694.925, + '0.005517419877137072' + ], + [ + 1495707754.925, + '0.006089813430348506' + ], + [ + 1495707814.925, + '0.00466754476190479' + ], + [ + 1495707874.925, + '0.006059954380517721' + ], + [ + 1495707934.925, + '0.005085657142856972' + ], + [ + 1495707994.925, + '0.005897665238095296' + ], + [ + 1495708054.925, + '0.0062282023199555885' + ], + [ + 1495708114.925, + '0.00526214553236979' + ], + [ + 1495708174.925, + '0.0044803300000000644' + ], + [ + 1495708234.925, + '0.005421443333333592' + ], + [ + 1495708294.925, + '0.005694326244512144' + ], + [ + 1495708354.925, + '0.005527721904761457' + ], + [ + 1495708414.925, + '0.005988819523809819' + ], + [ + 1495708474.925, + '0.005484704285714448' + ], + [ + 1495708534.925, + '0.005041123649230085' + ], + [ + 1495708594.925, + '0.005717767639612059' + ], + [ + 1495708654.925, + '0.005412954417342863' + ], + [ + 1495708714.925, + '0.005833343333333254' + ], + [ + 1495708774.925, + '0.005448135238094969' + ], + [ + 1495708834.925, + '0.005117341428571432' + ], + [ + 1495708894.925, + '0.005888345825277833' + ], + [ + 1495708954.925, + '0.005398543809524135' + ], + [ + 1495709014.925, + '0.005325611428571416' + ], + [ + 1495709074.925, + '0.005848668571428527' + ], + [ + 1495709134.925, + '0.005135003105145044' + ], + [ + 1495709194.925, + '0.0054551400000003' + ], + [ + 1495709254.925, + '0.005319472937322171' + ], + [ + 1495709314.925, + '0.00585677857142792' + ], + [ + 1495709374.925, + '0.0062146261904759215' + ], + [ + 1495709434.925, + '0.0067105060904182265' + ], + [ + 1495709494.925, + '0.005829691904762108' + ], + [ + 1495709554.925, + '0.005719280952381261' + ], + [ + 1495709614.925, + '0.005682603793416407' + ], + [ + 1495709674.925, + '0.0055272846277326934' + ], + [ + 1495709734.925, + '0.0057123680952386735' + ], + [ + 1495709794.925, + '0.00520597958075818' + ], + [ + 1495709854.925, + '0.005584358957263837' + ], + [ + 1495709914.925, + '0.005601104275197466' + ], + [ + 1495709974.925, + '0.005991657142857066' + ], + [ + 1495710034.925, + '0.00553722238095218' + ], + [ + 1495710094.925, + '0.005127883122696293' + ], + [ + 1495710154.925, + '0.005498111927534584' + ], + [ + 1495710214.925, + '0.005609934069084202' + ], + [ + 1495710274.925, + '0.00459206285714307' + ], + [ + 1495710334.925, + '0.0047910828571428084' + ], + [ + 1495710394.925, + '0.0056014671288845685' + ], + [ + 1495710454.925, + '0.005686936791078528' + ], + [ + 1495710514.925, + '0.00444480476190448' + ], + [ + 1495710574.925, + '0.005780394696738921' + ], + [ + 1495710634.925, + '0.0053107227550210365' + ], + [ + 1495710694.925, + '0.005096031495761817' + ], + [ + 1495710754.925, + '0.005451377979091524' + ], + [ + 1495710814.925, + '0.005328136666667083' + ], + [ + 1495710874.925, + '0.006020612857143043' + ], + [ + 1495710934.925, + '0.0061063585714285365' + ], + [ + 1495710994.925, + '0.006018346015752312' + ], + [ + 1495711054.925, + '0.005069130952381193' + ], + [ + 1495711114.925, + '0.005458406190476052' + ], + [ + 1495711174.925, + '0.00577219190476179' + ], + [ + 1495711234.925, + '0.005760814645658314' + ], + [ + 1495711294.925, + '0.005371875716579101' + ], + [ + 1495711354.925, + '0.0064232666666665834' + ], + [ + 1495711414.925, + '0.009369806836906667' + ], + [ + 1495711474.925, + '0.008956864761904692' + ], + [ + 1495711534.925, + '0.005266849368559271' + ], + [ + 1495711594.925, + '0.005335111364934262' + ], + [ + 1495711654.925, + '0.006461778319586945' + ], + [ + 1495711714.925, + '0.004687939890762393' + ], + [ + 1495711774.925, + '0.004438831245760684' + ], + [ + 1495711834.925, + '0.005142786666666613' + ], + [ + 1495711894.925, + '0.007257734212054963' + ], + [ + 1495711954.925, + '0.005621991904761494' + ], + [ + 1495712014.925, + '0.007868689999999862' + ], + [ + 1495712074.925, + '0.00910970215275738' + ], + [ + 1495712134.925, + '0.006151004285714278' + ], + [ + 1495712194.925, + '0.005447120924961522' + ], + [ + 1495712254.925, + '0.005150705153929503' + ], + [ + 1495712314.925, + '0.006358108714969314' + ], + [ + 1495712374.925, + '0.0057725354795696475' + ], + [ + 1495712434.925, + '0.005232139047619015' + ], + [ + 1495712494.925, + '0.004932809617949037' + ], + [ + 1495712554.925, + '0.004511607508499662' + ], + [ + 1495712614.925, + '0.00440487701522666' + ], + [ + 1495712674.925, + '0.005479113333333174' + ], + [ + 1495712734.925, + '0.004726317619047547' + ], + [ + 1495712794.925, + '0.005582041102958029' + ], + [ + 1495712854.925, + '0.006381481216082099' + ], + [ + 1495712914.925, + '0.005474260014095208' + ], + [ + 1495712974.925, + '0.00567597142857188' + ], + [ + 1495713034.925, + '0.0064741233333332985' + ], + [ + 1495713094.925, + '0.005467475714285271' + ], + [ + 1495713154.925, + '0.004868648393824457' + ], + [ + 1495713214.925, + '0.005254923286444893' + ], + [ + 1495713274.925, + '0.005599217150312865' + ], + [ + 1495713334.925, + '0.005105413720618919' + ], + [ + 1495713394.925, + '0.007246073333333279' + ], + [ + 1495713454.925, + '0.005990312380952272' + ], + [ + 1495713514.925, + '0.005594601853351101' + ], + [ + 1495713574.925, + '0.004739258673727054' + ], + [ + 1495713634.925, + '0.003932121428571783' + ], + [ + 1495713694.925, + '0.005018188268459395' + ], + [ + 1495713754.925, + '0.004538238095237985' + ], + [ + 1495713814.925, + '0.00561816643265435' + ], + [ + 1495713874.925, + '0.0063132584495033586' + ], + [ + 1495713934.925, + '0.00442385238095213' + ], + [ + 1495713994.925, + '0.004181795887658453' + ], + [ + 1495714054.925, + '0.004437759047619037' + ], + [ + 1495714114.925, + '0.006421748157178241' + ], + [ + 1495714174.925, + '0.006525143809523842' + ], + [ + 1495714234.925, + '0.004715904935144247' + ], + [ + 1495714294.925, + '0.005966040152763461' + ], + [ + 1495714354.925, + '0.005614535466921674' + ], + [ + 1495714414.925, + '0.004934375119415906' + ], + [ + 1495714474.925, + '0.0054122933333327385' + ], + [ + 1495714534.925, + '0.004926540699612279' + ], + [ + 1495714594.925, + '0.006124649517134237' + ], + [ + 1495714654.925, + '0.004629427092013995' + ], + [ + 1495714714.925, + '0.005117951257607005' + ], + [ + 1495714774.925, + '0.004868774512685422' + ], + [ + 1495714834.925, + '0.005310093333333399' + ], + [ + 1495714894.925, + '0.0054907752286127345' + ], + [ + 1495714954.925, + '0.004597678117351089' + ], + [ + 1495715014.925, + '0.0059622552380952' + ], + [ + 1495715074.925, + '0.005352457072655368' + ], + [ + 1495715134.925, + '0.005491630952381143' + ], + [ + 1495715194.925, + '0.006391770078379791' + ], + [ + 1495715254.925, + '0.005933472857142518' + ], + [ + 1495715314.925, + '0.005301314285714163' + ], + [ + 1495715374.925, + '0.0058352959724814165' + ], + [ + 1495715434.925, + '0.006154755147867044' + ], + [ + 1495715494.925, + '0.009391935637482038' + ], + [ + 1495715554.925, + '0.007846462857142592' + ], + [ + 1495715614.925, + '0.00477608215316353' + ], + [ + 1495715674.925, + '0.006132865238094998' + ], + [ + 1495715734.925, + '0.006159762457649516' + ], + [ + 1495715794.925, + '0.005957307073265968' + ], + [ + 1495715854.925, + '0.006652319091792501' + ], + [ + 1495715914.925, + '0.005493557402895287' + ], + [ + 1495715974.925, + '0.0058652434829145166' + ], + [ + 1495716034.925, + '0.005627400430468021' + ], + [ + 1495716094.925, + '0.006240656190475609' + ], + [ + 1495716154.925, + '0.006305997676168624' + ], + [ + 1495716214.925, + '0.005388057732783248' + ], + [ + 1495716274.925, + '0.0052814916048421244' + ], + [ + 1495716334.925, + '0.00699498614272497' + ], + [ + 1495716394.925, + '0.00627768693035141' + ], + [ + 1495716454.925, + '0.0042411487048161145' + ], + [ + 1495716514.925, + '0.005348647473627653' + ], + [ + 1495716574.925, + '0.0047176657142853975' + ], + [ + 1495716634.925, + '0.004437898571428686' + ], + [ + 1495716694.925, + '0.004923527366927261' + ], + [ + 1495716754.925, + '0.005131935066048421' + ], + [ + 1495716814.925, + '0.005046949523809611' + ], + [ + 1495716874.925, + '0.00547184095238092' + ], + [ + 1495716934.925, + '0.005224140016380444' + ], + [ + 1495716994.925, + '0.005297991171665292' + ], + [ + 1495717054.925, + '0.005492965995623498' + ], + [ + 1495717114.925, + '0.005754660000000403' + ], + [ + 1495717174.925, + '0.005949557138639285' + ], + [ + 1495717234.925, + '0.006091816112534666' + ], + [ + 1495717294.925, + '0.005554210080192063' + ], + [ + 1495717354.925, + '0.006411504395279871' + ], + [ + 1495717414.925, + '0.006319643996609606' + ], + [ + 1495717474.925, + '0.005539174405717675' + ], + [ + 1495717534.925, + '0.0053157078842772255' + ], + [ + 1495717594.925, + '0.005247480952381066' + ], + [ + 1495717654.925, + '0.004820141620396252' + ], + [ + 1495717714.925, + '0.005906173868322844' + ], + [ + 1495717774.925, + '0.006173117219570961' + ], + [ + 1495717834.925, + '0.005963340952380661' + ], + [ + 1495717894.925, + '0.005698976627681527' + ], + [ + 1495717954.925, + '0.004751279096346378' + ], + [ + 1495718014.925, + '0.005733142379359711' + ], + [ + 1495718074.925, + '0.004831689010348035' + ], + [ + 1495718134.925, + '0.005188370476191092' + ], + [ + 1495718194.925, + '0.004793227554547938' + ], + [ + 1495718254.925, + '0.003997442857142731' + ], + [ + 1495718314.925, + '0.004386040132951264' + ] + ] + } + ] + } + ] + } + ] + } + ], + 'last_update': '2017-05-25T13:18:34.949Z' +}; + +export default metricsGroupsAPIResponse; + +const responseMockData = { + 'GET': { + '/root/hello-prometheus/environments/30/additional_metrics.json': metricsGroupsAPIResponse, + 'http://test.host/frontend-fixtures/environments-project/environments/1/additional_metrics.json': metricsGroupsAPIResponse, // TODO: MAke sure this works in the monitoring_bundle_spec + }, +}; + +export const deploymentData = [ + { + id: 111, + iid: 3, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + ref: { + name: 'master' + }, + created_at: '2017-05-31T21:23:37.881Z', + tag: false, + 'last?': true + }, + { + id: 110, + iid: 2, + sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + ref: { + name: 'master' + }, + created_at: '2017-05-30T20:08:04.629Z', + tag: false, + 'last?': false + }, + { + id: 109, + iid: 1, + sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2', + ref: { + name: 'update2-readme' + }, + created_at: '2017-05-30T17:42:38.409Z', + tag: false, + 'last?': false + } +]; + +export const statePaths = { + settingsPath: '/root/hello-prometheus/services/prometheus/edit', + documentationPath: '/help/administration/monitoring/prometheus/index.md', +}; + +export const singleRowMetrics = [ + { + 'title': 'CPU usage', + 'weight': 1, + 'y_label': 'Values', + 'queries': [ + { + 'query_range': 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', + 'result': [ + { + 'metric': { + + }, + 'values': [ + { + 'time': '2017-06-04T21:22:59.508Z', + 'value': '0.06335544298150002' + }, + { + 'time': '2017-06-04T21:23:59.508Z', + 'value': '0.0420347312480917' + }, + { + 'time': '2017-06-04T21:24:59.508Z', + 'value': '0.0023175131665412706' + }, + { + 'time': '2017-06-04T21:25:59.508Z', + 'value': '0.002315870476190476' + }, + { + 'time': '2017-06-04T21:26:59.508Z', + 'value': '0.0025005961904761894' + }, + { + 'time': '2017-06-04T21:27:59.508Z', + 'value': '0.0024612605834341264' + }, + { + 'time': '2017-06-04T21:28:59.508Z', + 'value': '0.002313129398767631' + }, + { + 'time': '2017-06-04T21:29:59.508Z', + 'value': '0.002411067353663882' + }, + { + 'time': '2017-06-04T21:30:59.508Z', + 'value': '0.002577309263721303' + }, + { + 'time': '2017-06-04T21:31:59.508Z', + 'value': '0.00242688307730403' + }, + { + 'time': '2017-06-04T21:32:59.508Z', + 'value': '0.0024168360301330457' + }, + { + 'time': '2017-06-04T21:33:59.508Z', + 'value': '0.0020449528090743714' + }, + { + 'time': '2017-06-04T21:34:59.508Z', + 'value': '0.0019149619047619036' + }, + { + 'time': '2017-06-04T21:35:59.508Z', + 'value': '0.0024491714364625094' + }, + { + 'time': '2017-06-04T21:36:59.508Z', + 'value': '0.002728773131172677' + }, + { + 'time': '2017-06-04T21:37:59.508Z', + 'value': '0.0028439119047618997' + }, + { + 'time': '2017-06-04T21:38:59.508Z', + 'value': '0.0026307480952380917' + }, + { + 'time': '2017-06-04T21:39:59.508Z', + 'value': '0.0025024842620546446' + }, + { + 'time': '2017-06-04T21:40:59.508Z', + 'value': '0.002300662387260825' + }, + { + 'time': '2017-06-04T21:41:59.508Z', + 'value': '0.002052890924848337' + }, + { + 'time': '2017-06-04T21:42:59.508Z', + 'value': '0.0023711195238095275' + }, + { + 'time': '2017-06-04T21:43:59.508Z', + 'value': '0.002513477619047618' + }, + { + 'time': '2017-06-04T21:44:59.508Z', + 'value': '0.0023489776287844897' + }, + { + 'time': '2017-06-04T21:45:59.508Z', + 'value': '0.002542572310212481' + }, + { + 'time': '2017-06-04T21:46:59.508Z', + 'value': '0.0024579470671707952' + }, + { + 'time': '2017-06-04T21:47:59.508Z', + 'value': '0.0028725150236664403' + }, + { + 'time': '2017-06-04T21:48:59.508Z', + 'value': '0.0024356089105610525' + }, + { + 'time': '2017-06-04T21:49:59.508Z', + 'value': '0.002544015828269929' + }, + { + 'time': '2017-06-04T21:50:59.508Z', + 'value': '0.0029595013380824906' + }, + { + 'time': '2017-06-04T21:51:59.508Z', + 'value': '0.0023084015085858' + }, + { + 'time': '2017-06-04T21:52:59.508Z', + 'value': '0.0021070500000000083' + }, + { + 'time': '2017-06-04T21:53:59.508Z', + 'value': '0.0022950066191106617' + }, + { + 'time': '2017-06-04T21:54:59.508Z', + 'value': '0.002492719454470995' + }, + { + 'time': '2017-06-04T21:55:59.508Z', + 'value': '0.00244312761904762' + }, + { + 'time': '2017-06-04T21:56:59.508Z', + 'value': '0.0023495500000000028' + }, + { + 'time': '2017-06-04T21:57:59.508Z', + 'value': '0.0020597072353070005' + }, + { + 'time': '2017-06-04T21:58:59.508Z', + 'value': '0.0021482352044800866' + }, + { + 'time': '2017-06-04T21:59:59.508Z', + 'value': '0.002333490000000004' + }, + { + 'time': '2017-06-04T22:00:59.508Z', + 'value': '0.0025899442857142815' + }, + { + 'time': '2017-06-04T22:01:59.508Z', + 'value': '0.002430299999999999' + }, + { + 'time': '2017-06-04T22:02:59.508Z', + 'value': '0.0023550328092113476' + }, + { + 'time': '2017-06-04T22:03:59.508Z', + 'value': '0.0026521871636872793' + }, + { + 'time': '2017-06-04T22:04:59.508Z', + 'value': '0.0023080671428571398' + }, + { + 'time': '2017-06-04T22:05:59.508Z', + 'value': '0.0024108401032390896' + }, + { + 'time': '2017-06-04T22:06:59.508Z', + 'value': '0.002433249366678738' + }, + { + 'time': '2017-06-04T22:07:59.508Z', + 'value': '0.0023242202306688682' + }, + { + 'time': '2017-06-04T22:08:59.508Z', + 'value': '0.002388222857142859' + }, + { + 'time': '2017-06-04T22:09:59.508Z', + 'value': '0.002115974914046794' + }, + { + 'time': '2017-06-04T22:10:59.508Z', + 'value': '0.0025090043331269917' + }, + { + 'time': '2017-06-04T22:11:59.508Z', + 'value': '0.002445507057277277' + }, + { + 'time': '2017-06-04T22:12:59.508Z', + 'value': '0.0026348773751130976' + }, + { + 'time': '2017-06-04T22:13:59.508Z', + 'value': '0.0025616258583088104' + }, + { + 'time': '2017-06-04T22:14:59.508Z', + 'value': '0.0021544093415751505' + }, + { + 'time': '2017-06-04T22:15:59.508Z', + 'value': '0.002649394767668881' + }, + { + 'time': '2017-06-04T22:16:59.508Z', + 'value': '0.0024023332666685705' + }, + { + 'time': '2017-06-04T22:17:59.508Z', + 'value': '0.0025444105294235306' + }, + { + 'time': '2017-06-04T22:18:59.508Z', + 'value': '0.0027298872305772806' + }, + { + 'time': '2017-06-04T22:19:59.508Z', + 'value': '0.0022880104956379287' + }, + { + 'time': '2017-06-04T22:20:59.508Z', + 'value': '0.002473246666666661' + }, + { + 'time': '2017-06-04T22:21:59.508Z', + 'value': '0.002259948381935587' + }, + { + 'time': '2017-06-04T22:22:59.508Z', + 'value': '0.0025778470886268835' + }, + { + 'time': '2017-06-04T22:23:59.508Z', + 'value': '0.002246127910852894' + }, + { + 'time': '2017-06-04T22:24:59.508Z', + 'value': '0.0020697466666666758' + }, + { + 'time': '2017-06-04T22:25:59.508Z', + 'value': '0.00225859722473547' + }, + { + 'time': '2017-06-04T22:26:59.508Z', + 'value': '0.0026466728254554814' + }, + { + 'time': '2017-06-04T22:27:59.508Z', + 'value': '0.002151247619047619' + }, + { + 'time': '2017-06-04T22:28:59.508Z', + 'value': '0.002324161444543914' + }, + { + 'time': '2017-06-04T22:29:59.508Z', + 'value': '0.002476474313796452' + }, + { + 'time': '2017-06-04T22:30:59.508Z', + 'value': '0.0023922184232080517' + }, + { + 'time': '2017-06-04T22:31:59.508Z', + 'value': '0.0025094934237468933' + }, + { + 'time': '2017-06-04T22:32:59.508Z', + 'value': '0.0025665311098200883' + }, + { + 'time': '2017-06-04T22:33:59.508Z', + 'value': '0.0024154900681661374' + }, + { + 'time': '2017-06-04T22:34:59.508Z', + 'value': '0.0023267450166192037' + }, + { + 'time': '2017-06-04T22:35:59.508Z', + 'value': '0.002156521904761904' + }, + { + 'time': '2017-06-04T22:36:59.508Z', + 'value': '0.0025474356898637007' + }, + { + 'time': '2017-06-04T22:37:59.508Z', + 'value': '0.0025989409624670233' + }, + { + 'time': '2017-06-04T22:38:59.508Z', + 'value': '0.002348336664762987' + }, + { + 'time': '2017-06-04T22:39:59.508Z', + 'value': '0.002665888246554726' + }, + { + 'time': '2017-06-04T22:40:59.508Z', + 'value': '0.002652684787474174' + }, + { + 'time': '2017-06-04T22:41:59.508Z', + 'value': '0.002472620430865355' + }, + { + 'time': '2017-06-04T22:42:59.508Z', + 'value': '0.0020616469210110247' + }, + { + 'time': '2017-06-04T22:43:59.508Z', + 'value': '0.0022434546372311934' + }, + { + 'time': '2017-06-04T22:44:59.508Z', + 'value': '0.0024469386784827982' + }, + { + 'time': '2017-06-04T22:45:59.508Z', + 'value': '0.0026192823809523787' + }, + { + 'time': '2017-06-04T22:46:59.508Z', + 'value': '0.003451999542852798' + }, + { + 'time': '2017-06-04T22:47:59.508Z', + 'value': '0.0031780314285714288' + }, + { + 'time': '2017-06-04T22:48:59.508Z', + 'value': '0.0024403352380952415' + }, + { + 'time': '2017-06-04T22:49:59.508Z', + 'value': '0.001998824761904764' + }, + { + 'time': '2017-06-04T22:50:59.508Z', + 'value': '0.0023792404761904806' + }, + { + 'time': '2017-06-04T22:51:59.508Z', + 'value': '0.002725906190476185' + }, + { + 'time': '2017-06-04T22:52:59.508Z', + 'value': '0.0020989528671155624' + }, + { + 'time': '2017-06-04T22:53:59.508Z', + 'value': '0.00228808226745016' + }, + { + 'time': '2017-06-04T22:54:59.508Z', + 'value': '0.0019860807413192147' + }, + { + 'time': '2017-06-04T22:55:59.508Z', + 'value': '0.0022698085714285897' + }, + { + 'time': '2017-06-04T22:56:59.508Z', + 'value': '0.0022839098467604415' + }, + { + 'time': '2017-06-04T22:57:59.508Z', + 'value': '0.002531114761904749' + }, + { + 'time': '2017-06-04T22:58:59.508Z', + 'value': '0.0028941072550999016' + }, + { + 'time': '2017-06-04T22:59:59.508Z', + 'value': '0.002547169523809506' + }, + { + 'time': '2017-06-04T23:00:59.508Z', + 'value': '0.0024062999999999958' + }, + { + 'time': '2017-06-04T23:01:59.508Z', + 'value': '0.0026939518471604386' + }, + { + 'time': '2017-06-04T23:02:59.508Z', + 'value': '0.002362901428571429' + }, + { + 'time': '2017-06-04T23:03:59.508Z', + 'value': '0.002663927142857154' + }, + { + 'time': '2017-06-04T23:04:59.508Z', + 'value': '0.0026173314285714354' + }, + { + 'time': '2017-06-04T23:05:59.508Z', + 'value': '0.002326527366406044' + }, + { + 'time': '2017-06-04T23:06:59.508Z', + 'value': '0.002035313809523809' + }, + { + 'time': '2017-06-04T23:07:59.508Z', + 'value': '0.002421447414786533' + }, + { + 'time': '2017-06-04T23:08:59.508Z', + 'value': '0.002898313809523804' + }, + { + 'time': '2017-06-04T23:09:59.508Z', + 'value': '0.002544891856112907' + }, + { + 'time': '2017-06-04T23:10:59.508Z', + 'value': '0.002290625356938882' + }, + { + 'time': '2017-06-04T23:11:59.508Z', + 'value': '0.002483028095238096' + }, + { + 'time': '2017-06-04T23:12:59.508Z', + 'value': '0.0023396832350784237' + }, + { + 'time': '2017-06-04T23:13:59.508Z', + 'value': '0.002085529248176153' + }, + { + 'time': '2017-06-04T23:14:59.508Z', + 'value': '0.0022417815068428012' + }, + { + 'time': '2017-06-04T23:15:59.508Z', + 'value': '0.002660293333333341' + }, + { + 'time': '2017-06-04T23:16:59.508Z', + 'value': '0.0029845149093818226' + }, + { + 'time': '2017-06-04T23:17:59.508Z', + 'value': '0.0027716655079475464' + }, + { + 'time': '2017-06-04T23:18:59.508Z', + 'value': '0.0025217708908741128' + }, + { + 'time': '2017-06-04T23:19:59.508Z', + 'value': '0.0025811235131094055' + }, + { + 'time': '2017-06-04T23:20:59.508Z', + 'value': '0.002209904761904762' + }, + { + 'time': '2017-06-04T23:21:59.508Z', + 'value': '0.0025053322926383344' + }, + { + 'time': '2017-06-04T23:22:59.508Z', + 'value': '0.002350917636526411' + }, + { + 'time': '2017-06-04T23:23:59.508Z', + 'value': '0.0018477500000000078' + }, + { + 'time': '2017-06-04T23:24:59.508Z', + 'value': '0.002427629523809527' + }, + { + 'time': '2017-06-04T23:25:59.508Z', + 'value': '0.0019305498147601655' + }, + { + 'time': '2017-06-04T23:26:59.508Z', + 'value': '0.002097250000000006' + }, + { + 'time': '2017-06-04T23:27:59.508Z', + 'value': '0.002675020952780041' + }, + { + 'time': '2017-06-04T23:28:59.508Z', + 'value': '0.0023142214285714374' + }, + { + 'time': '2017-06-04T23:29:59.508Z', + 'value': '0.0023644723809523737' + }, + { + 'time': '2017-06-04T23:30:59.508Z', + 'value': '0.002108696190476198' + }, + { + 'time': '2017-06-04T23:31:59.508Z', + 'value': '0.0019918289697997194' + }, + { + 'time': '2017-06-04T23:32:59.508Z', + 'value': '0.001583584285714283' + }, + { + 'time': '2017-06-04T23:33:59.508Z', + 'value': '0.002073770226383112' + }, + { + 'time': '2017-06-04T23:34:59.508Z', + 'value': '0.0025877664234966818' + }, + { + 'time': '2017-06-04T23:35:59.508Z', + 'value': '0.0021138238095238147' + }, + { + 'time': '2017-06-04T23:36:59.508Z', + 'value': '0.0022140838095238303' + }, + { + 'time': '2017-06-04T23:37:59.508Z', + 'value': '0.0018592674425248847' + }, + { + 'time': '2017-06-04T23:38:59.508Z', + 'value': '0.0020461969533657016' + }, + { + 'time': '2017-06-04T23:39:59.508Z', + 'value': '0.0021593628571428543' + }, + { + 'time': '2017-06-04T23:40:59.508Z', + 'value': '0.0024330682564928188' + }, + { + 'time': '2017-06-04T23:41:59.508Z', + 'value': '0.0021501804779093174' + }, + { + 'time': '2017-06-04T23:42:59.508Z', + 'value': '0.0025787493928397945' + }, + { + 'time': '2017-06-04T23:43:59.508Z', + 'value': '0.002593657082448396' + }, + { + 'time': '2017-06-04T23:44:59.508Z', + 'value': '0.0021316752380952306' + }, + { + 'time': '2017-06-04T23:45:59.508Z', + 'value': '0.0026972905019952086' + }, + { + 'time': '2017-06-04T23:46:59.508Z', + 'value': '0.002580250764292983' + }, + { + 'time': '2017-06-04T23:47:59.508Z', + 'value': '0.00227103000000001' + }, + { + 'time': '2017-06-04T23:48:59.508Z', + 'value': '0.0023678515647321146' + }, + { + 'time': '2017-06-04T23:49:59.508Z', + 'value': '0.002371472857142866' + }, + { + 'time': '2017-06-04T23:50:59.508Z', + 'value': '0.0026181353688500978' + }, + { + 'time': '2017-06-04T23:51:59.508Z', + 'value': '0.0025609667711121217' + }, + { + 'time': '2017-06-04T23:52:59.508Z', + 'value': '0.0027145308139922557' + }, + { + 'time': '2017-06-04T23:53:59.508Z', + 'value': '0.0024249397613310512' + }, + { + 'time': '2017-06-04T23:54:59.508Z', + 'value': '0.002399907142857147' + }, + { + 'time': '2017-06-04T23:55:59.508Z', + 'value': '0.0024753357142857195' + }, + { + 'time': '2017-06-04T23:56:59.508Z', + 'value': '0.0026179149325231575' + }, + { + 'time': '2017-06-04T23:57:59.508Z', + 'value': '0.0024261340368186956' + }, + { + 'time': '2017-06-04T23:58:59.508Z', + 'value': '0.0021061071428571517' + }, + { + 'time': '2017-06-04T23:59:59.508Z', + 'value': '0.0024033971105037015' + }, + { + 'time': '2017-06-05T00:00:59.508Z', + 'value': '0.0028287676190475956' + }, + { + 'time': '2017-06-05T00:01:59.508Z', + 'value': '0.002499719050294778' + }, + { + 'time': '2017-06-05T00:02:59.508Z', + 'value': '0.0026726102153353856' + }, + { + 'time': '2017-06-05T00:03:59.508Z', + 'value': '0.00262582619047618' + }, + { + 'time': '2017-06-05T00:04:59.508Z', + 'value': '0.002280473147363316' + }, + { + 'time': '2017-06-05T00:05:59.508Z', + 'value': '0.002095581470652675' + }, + { + 'time': '2017-06-05T00:06:59.508Z', + 'value': '0.002270768490828408' + }, + { + 'time': '2017-06-05T00:07:59.508Z', + 'value': '0.002728577415023017' + }, + { + 'time': '2017-06-05T00:08:59.508Z', + 'value': '0.002652512857142863' + }, + { + 'time': '2017-06-05T00:09:59.508Z', + 'value': '0.0022781033924455674' + }, + { + 'time': '2017-06-05T00:10:59.508Z', + 'value': '0.0025345038095238234' + }, + { + 'time': '2017-06-05T00:11:59.508Z', + 'value': '0.002376050020000397' + }, + { + 'time': '2017-06-05T00:12:59.508Z', + 'value': '0.002455068143506122' + }, + { + 'time': '2017-06-05T00:13:59.508Z', + 'value': '0.002826705714285719' + }, + { + 'time': '2017-06-05T00:14:59.508Z', + 'value': '0.002343833692070314' + }, + { + 'time': '2017-06-05T00:15:59.508Z', + 'value': '0.00264853297122164' + }, + { + 'time': '2017-06-05T00:16:59.508Z', + 'value': '0.0027656335117426257' + }, + { + 'time': '2017-06-05T00:17:59.508Z', + 'value': '0.0025896543842439564' + }, + { + 'time': '2017-06-05T00:18:59.508Z', + 'value': '0.002180053237081201' + }, + { + 'time': '2017-06-05T00:19:59.508Z', + 'value': '0.002475245002333342' + }, + { + 'time': '2017-06-05T00:20:59.508Z', + 'value': '0.0027559767805101065' + }, + { + 'time': '2017-06-05T00:21:59.508Z', + 'value': '0.0022294836141296607' + }, + { + 'time': '2017-06-05T00:22:59.508Z', + 'value': '0.0021383590476190643' + }, + { + 'time': '2017-06-05T00:23:59.508Z', + 'value': '0.002085417956361494' + }, + { + 'time': '2017-06-05T00:24:59.508Z', + 'value': '0.0024140319047619013' + }, + { + 'time': '2017-06-05T00:25:59.508Z', + 'value': '0.0024513114285714304' + }, + { + 'time': '2017-06-05T00:26:59.508Z', + 'value': '0.0026932152380952446' + }, + { + 'time': '2017-06-05T00:27:59.508Z', + 'value': '0.0022656844350898517' + }, + { + 'time': '2017-06-05T00:28:59.508Z', + 'value': '0.0024483785714285704' + }, + { + 'time': '2017-06-05T00:29:59.508Z', + 'value': '0.002559505804817207' + }, + { + 'time': '2017-06-05T00:30:59.508Z', + 'value': '0.0019485681088751649' + }, + { + 'time': '2017-06-05T00:31:59.508Z', + 'value': '0.00228367984456996' + }, + { + 'time': '2017-06-05T00:32:59.508Z', + 'value': '0.002522149047619049' + }, + { + 'time': '2017-06-05T00:33:59.508Z', + 'value': '0.0026860117715406737' + }, + { + 'time': '2017-06-05T00:34:59.508Z', + 'value': '0.002679669523809523' + }, + { + 'time': '2017-06-05T00:35:59.508Z', + 'value': '0.0022201920970675937' + }, + { + 'time': '2017-06-05T00:36:59.508Z', + 'value': '0.0022917647619047615' + }, + { + 'time': '2017-06-05T00:37:59.508Z', + 'value': '0.0021774059294673576' + }, + { + 'time': '2017-06-05T00:38:59.508Z', + 'value': '0.0024637766666666763' + }, + { + 'time': '2017-06-05T00:39:59.508Z', + 'value': '0.002470468290174195' + }, + { + 'time': '2017-06-05T00:40:59.508Z', + 'value': '0.0022188616082057812' + }, + { + 'time': '2017-06-05T00:41:59.508Z', + 'value': '0.002421840744373875' + }, + { + 'time': '2017-06-05T00:42:59.508Z', + 'value': '0.0023918266666666547' + }, + { + 'time': '2017-06-05T00:43:59.508Z', + 'value': '0.002195743809523809' + }, + { + 'time': '2017-06-05T00:44:59.508Z', + 'value': '0.0025514828571428687' + }, + { + 'time': '2017-06-05T00:45:59.508Z', + 'value': '0.0027981709349612694' + }, + { + 'time': '2017-06-05T00:46:59.508Z', + 'value': '0.002557977142857146' + }, + { + 'time': '2017-06-05T00:47:59.508Z', + 'value': '0.002213244285714286' + }, + { + 'time': '2017-06-05T00:48:59.508Z', + 'value': '0.0025706738095238046' + }, + { + 'time': '2017-06-05T00:49:59.508Z', + 'value': '0.002210976666666671' + }, + { + 'time': '2017-06-05T00:50:59.508Z', + 'value': '0.002055377091646749' + }, + { + 'time': '2017-06-05T00:51:59.508Z', + 'value': '0.002308368095238119' + }, + { + 'time': '2017-06-05T00:52:59.508Z', + 'value': '0.0024687939885141615' + }, + { + 'time': '2017-06-05T00:53:59.508Z', + 'value': '0.002563018571428578' + }, + { + 'time': '2017-06-05T00:54:59.508Z', + 'value': '0.00240563291078959' + } + ] + } + ] + } + ] + }, + { + 'title': 'Memory usage', + 'weight': 1, + 'y_label': 'Values', + 'queries': [ + { + 'query_range': 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', + 'label': 'Container memory', + 'unit': 'MiB', + 'result': [ + { + 'metric': { + + }, + 'values': [ + { + 'time': '2017-06-04T21:22:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:23:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:24:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:25:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:26:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:27:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:28:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:29:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:30:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:31:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:32:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:33:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:34:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:35:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:36:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:37:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:38:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:39:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:40:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:41:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:42:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:43:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:44:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:45:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:46:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:47:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:48:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:49:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:50:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:51:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:52:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:53:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:54:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:55:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:56:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:57:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:58:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T21:59:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:00:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:01:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:02:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:03:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:04:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:05:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:06:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:07:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:08:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:09:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:10:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:11:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:12:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:13:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:14:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:15:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:16:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:17:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:18:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:19:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:20:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:21:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:22:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:23:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:24:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:25:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:26:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:27:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:28:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:29:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:30:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:31:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:32:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:33:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:34:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:35:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:36:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:37:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:38:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:39:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:40:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:41:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:42:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:43:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:44:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:45:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:46:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:47:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:48:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:49:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:50:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:51:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:52:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:53:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:54:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:55:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:56:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:57:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:58:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T22:59:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:00:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:01:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:02:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:03:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:04:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:05:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:06:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:07:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:08:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:09:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:10:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:11:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:12:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:13:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:14:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:15:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:16:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:17:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:18:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:19:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:20:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:21:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:22:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:23:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:24:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:25:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:26:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:27:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:28:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:29:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:30:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:31:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:32:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:33:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:34:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:35:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:36:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:37:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:38:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:39:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:40:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:41:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:42:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:43:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:44:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:45:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:46:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:47:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:48:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:49:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:50:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:51:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:52:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:53:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:54:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:55:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:56:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:57:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:58:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-04T23:59:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:00:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:01:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:02:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:03:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:04:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:05:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:06:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:07:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:08:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:09:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:10:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:11:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:12:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:13:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:14:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:15:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:16:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:17:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:18:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:19:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:20:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:21:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:22:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:23:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:24:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:25:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:26:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:27:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:28:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:29:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:30:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:31:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:32:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:33:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:34:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:35:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:36:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:37:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:38:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:39:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:40:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:41:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:42:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:43:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:44:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:45:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:46:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:47:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:48:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:49:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:50:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:51:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:52:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:53:59.508Z', + 'value': '15.0859375' + }, + { + 'time': '2017-06-05T00:54:59.508Z', + 'value': '15.0859375' + } + ] + } + ] + } + ] + } +]; + +export function MonitorMockInterceptor(request, next) { + const body = responseMockData[request.method.toUpperCase()][request.url]; + + next(request.respondWith(JSON.stringify(body), { + status: 200, + })); +} diff --git a/spec/javascripts/monitoring/monitoring_column_spec.js b/spec/javascripts/monitoring/monitoring_column_spec.js new file mode 100644 index 00000000000..c8787f9708c --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_column_spec.js @@ -0,0 +1,97 @@ +import Vue from 'vue'; +import _ from 'underscore'; +import MonitoringColumn from '~/monitoring/components/monitoring_column.vue'; +import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; +import eventHub from '~/monitoring/event_hub'; +import { deploymentData, singleRowMetrics } from './mock_data'; + +const createComponent = (propsData) => { + const Component = Vue.extend(MonitoringColumn); + + return new Component({ + propsData, + }).$mount(); +}; + +describe('MonitoringColumn', () => { + beforeEach(() => { + spyOn(MonitoringMixins.methods, 'formatDeployments').and.callFake(function fakeFormat() { + return {}; + }); + }); + + it('has a title', () => { + const component = createComponent({ + columnData: singleRowMetrics[0], + classType: 'col-md-6', + updateAspectRatio: false, + deploymentData, + }); + + expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.columnData.title); + }); + + it('creates a path for the line and area of the graph', (done) => { + const component = createComponent({ + columnData: singleRowMetrics[0], + classType: 'col-md-6', + updateAspectRatio: false, + deploymentData, + }); + + Vue.nextTick(() => { + expect(component.area).toBeDefined(); + expect(component.line).toBeDefined(); + expect(typeof component.area).toEqual('string'); + expect(typeof component.line).toEqual('string'); + expect(_.isFunction(component.xScale)).toBe(true); + expect(_.isFunction(component.yScale)).toBe(true); + done(); + }); + }); + + describe('Computed props', () => { + it('axisTransform translates an element Y position depending of its height', () => { + const component = createComponent({ + columnData: singleRowMetrics[0], + classType: 'col-md-6', + updateAspectRatio: false, + deploymentData, + }); + + const transformedHeight = `${component.graphHeight - 100}`; + expect(component.axisTransform.indexOf(transformedHeight)) + .not.toEqual(-1); + }); + + it('outterViewBox gets a width and height property based on the DOM size of the element', () => { + const component = createComponent({ + columnData: singleRowMetrics[0], + classType: 'col-md-6', + updateAspectRatio: false, + deploymentData, + }); + + const viewBoxArray = component.outterViewBox.split(' '); + expect(typeof component.outterViewBox).toEqual('string'); + expect(viewBoxArray[2]).toEqual(component.graphWidth.toString()); + expect(viewBoxArray[3]).toEqual(component.graphHeight.toString()); + }); + }); + + it('sends an event to the eventhub when it has finished resizing', (done) => { + const component = createComponent({ + columnData: singleRowMetrics[0], + classType: 'col-md-6', + updateAspectRatio: false, + deploymentData, + }); + spyOn(eventHub, '$emit'); + + component.updateAspectRatio = true; + Vue.nextTick(() => { + expect(eventHub.$emit).toHaveBeenCalled(); + done(); + }); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_deployment_spec.js b/spec/javascripts/monitoring/monitoring_deployment_spec.js new file mode 100644 index 00000000000..5cc5b514824 --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_deployment_spec.js @@ -0,0 +1,137 @@ +import Vue from 'vue'; +import MonitoringState from '~/monitoring/components/monitoring_deployment.vue'; +import { deploymentData } from './mock_data'; + +const createComponent = (propsData) => { + const Component = Vue.extend(MonitoringState); + + return new Component({ + propsData, + }).$mount(); +}; + +describe('MonitoringDeployment', () => { + const reducedDeploymentData = [deploymentData[0]]; + reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name; + reducedDeploymentData[0].xPos = 10; + reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at); + describe('Methods', () => { + it('refText shows the ref when a tag is available', () => { + reducedDeploymentData[0].tag = '1.0'; + const component = createComponent({ + showDeployInfo: false, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect( + component.refText(reducedDeploymentData[0]), + ).toEqual(reducedDeploymentData[0].ref); + }); + + it('refText shows the sha when no tag is available', () => { + reducedDeploymentData[0].tag = null; + const component = createComponent({ + showDeployInfo: false, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect( + component.refText(reducedDeploymentData[0]), + ).toContain('f5bcd1'); + }); + + it('nameDeploymentClass creates a class with the prefix deploy-info-', () => { + const component = createComponent({ + showDeployInfo: false, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect( + component.nameDeploymentClass(reducedDeploymentData[0]), + ).toContain('deploy-info'); + }); + + it('transformDeploymentGroup translates an available deployment', () => { + const component = createComponent({ + showDeployInfo: false, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect( + component.transformDeploymentGroup(reducedDeploymentData[0]), + ).toContain('translate(11, 20)'); + }); + + it('hides the deployment flag', () => { + reducedDeploymentData[0].showDeploymentFlag = false; + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull(); + }); + + it('shows the deployment flag', () => { + reducedDeploymentData[0].showDeploymentFlag = true; + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect( + component.$el.querySelector('.js-deploy-info-box').style.display, + ).not.toEqual('display: none;'); + }); + + it('shows the refText inside a text element with the deploy-info-text class', () => { + reducedDeploymentData[0].showDeploymentFlag = true; + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect( + component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(), + ).toEqual(component.refText(reducedDeploymentData[0])); + }); + + it('should contain a hidden gradient', () => { + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); + }); + + describe('Computed props', () => { + it('calculatedHeight', () => { + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect(component.calculatedHeight).toEqual(180); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_flag_spec.js b/spec/javascripts/monitoring/monitoring_flag_spec.js new file mode 100644 index 00000000000..3861a95ff07 --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_flag_spec.js @@ -0,0 +1,76 @@ +import Vue from 'vue'; +import MonitoringFlag from '~/monitoring/components/monitoring_flag.vue'; + +const createComponent = (propsData) => { + const Component = Vue.extend(MonitoringFlag); + + return new Component({ + propsData, + }).$mount(); +}; + +function getCoordinate(component, selector, coordinate) { + const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate); + return parseInt(coordinateVal, 10); +} + +describe('MonitoringFlag', () => { + it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { + const component = createComponent({ + currentXCoordinate: 200, + currentYCoordinate: 100, + currentFlagPosition: 100, + currentData: { + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect(getCoordinate(component, '.selected-metric-line', 'x1')) + .toEqual(component.currentXCoordinate); + expect(getCoordinate(component, '.selected-metric-line', 'x2')) + .toEqual(component.currentXCoordinate); + expect(getCoordinate(component, '.circle-metric', 'cx')) + .toEqual(component.currentXCoordinate); + expect(getCoordinate(component, '.circle-metric', 'cy')) + .toEqual(component.currentYCoordinate); + }); + + it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { + const component = createComponent({ + currentXCoordinate: 200, + currentYCoordinate: 100, + currentFlagPosition: 100, + currentData: { + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }, + graphHeight: 300, + graphHeightOffset: 120, + }); + + const svg = component.$el.querySelector('.rect-text-metric'); + expect(svg.tagName).toEqual('svg'); + expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition); + }); + + describe('Computed props', () => { + it('calculatedHeight', () => { + const component = createComponent({ + currentXCoordinate: 200, + currentYCoordinate: 100, + currentFlagPosition: 100, + currentData: { + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }, + graphHeight: 300, + graphHeightOffset: 120, + }); + + expect(component.calculatedHeight).toEqual(180); + }); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_legends_spec.js b/spec/javascripts/monitoring/monitoring_legends_spec.js new file mode 100644 index 00000000000..4c69b81e650 --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_legends_spec.js @@ -0,0 +1,111 @@ +import Vue from 'vue'; +import MonitoringLegends from '~/monitoring/components/monitoring_legends.vue'; +import measurements from '~/monitoring/utils/measurements'; + +const createComponent = (propsData) => { + const Component = Vue.extend(MonitoringLegends); + + return new Component({ + propsData, + }).$mount(); +}; + +function getTextFromNode(component, selector) { + return component.$el.querySelector(selector).firstChild.nodeValue.trim(); +} + +describe('MonitoringLegends', () => { + describe('Computed props', () => { + it('textTransform', () => { + const component = createComponent({ + graphWidth: 500, + graphHeight: 300, + margin: measurements.large.margin, + measurements: measurements.large, + areaColorRgb: '#f0f0f0', + legendTitle: 'Title', + yAxisLabel: 'Values', + metricUsage: 'Value', + }); + + expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); + }); + + it('xPosition', () => { + const component = createComponent({ + graphWidth: 500, + graphHeight: 300, + margin: measurements.large.margin, + measurements: measurements.large, + areaColorRgb: '#f0f0f0', + legendTitle: 'Title', + yAxisLabel: 'Values', + metricUsage: 'Value', + }); + + expect(component.xPosition).toEqual(180); + }); + + it('yPosition', () => { + const component = createComponent({ + graphWidth: 500, + graphHeight: 300, + margin: measurements.large.margin, + measurements: measurements.large, + areaColorRgb: '#f0f0f0', + legendTitle: 'Title', + yAxisLabel: 'Values', + metricUsage: 'Value', + }); + + expect(component.yPosition).toEqual(240); + }); + + it('rectTransform', () => { + const component = createComponent({ + graphWidth: 500, + graphHeight: 300, + margin: measurements.large.margin, + measurements: measurements.large, + areaColorRgb: '#f0f0f0', + legendTitle: 'Title', + yAxisLabel: 'Values', + metricUsage: 'Value', + }); + + expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); + }); + }); + + it('has 2 rect-axis-text rect svg elements', () => { + const component = createComponent({ + graphWidth: 500, + graphHeight: 300, + margin: measurements.large.margin, + measurements: measurements.large, + areaColorRgb: '#f0f0f0', + legendTitle: 'Title', + yAxisLabel: 'Values', + metricUsage: 'Value', + }); + + expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); + }); + + it('contains text to signal the usage, title and time', () => { + const component = createComponent({ + graphWidth: 500, + graphHeight: 300, + margin: measurements.large.margin, + measurements: measurements.large, + areaColorRgb: '#f0f0f0', + legendTitle: 'Title', + yAxisLabel: 'Values', + metricUsage: 'Value', + }); + + expect(getTextFromNode(component, '.text-metric-title')).toEqual(component.legendTitle); + expect(getTextFromNode(component, '.text-metric-usage')).toEqual(component.metricUsage); + expect(getTextFromNode(component, '.label-axis-text')).toEqual(component.yAxisLabel); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_row_spec.js b/spec/javascripts/monitoring/monitoring_row_spec.js new file mode 100644 index 00000000000..a82480e8342 --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_row_spec.js @@ -0,0 +1,57 @@ +import Vue from 'vue'; +import MonitoringRow from '~/monitoring/components/monitoring_row.vue'; +import { deploymentData, singleRowMetrics } from './mock_data'; + +const createComponent = (propsData) => { + const Component = Vue.extend(MonitoringRow); + + return new Component({ + propsData, + }).$mount(); +}; + +describe('MonitoringRow', () => { + describe('Computed props', () => { + it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => { + const component = createComponent({ + rowData: singleRowMetrics, + updateAspectRatio: false, + deploymentData, + }); + + expect(component.bootstrapClass).toEqual('col-md-6'); + }); + + it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => { + const component = createComponent({ + rowData: [singleRowMetrics[0]], + updateAspectRatio: false, + deploymentData, + }); + + expect(component.bootstrapClass).toEqual('col-md-12'); + }); + }); + + it('has one column', () => { + const component = createComponent({ + rowData: singleRowMetrics, + updateAspectRatio: false, + deploymentData, + }); + + expect(component.$el.querySelectorAll('.prometheus-svg-container').length) + .toEqual(component.rowData.length); + }); + + it('has two columns', () => { + const component = createComponent({ + rowData: singleRowMetrics, + updateAspectRatio: false, + deploymentData, + }); + + expect(component.$el.querySelectorAll('.col-md-6').length) + .toEqual(component.rowData.length); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_spec.js b/spec/javascripts/monitoring/monitoring_spec.js new file mode 100644 index 00000000000..6c7b691baa4 --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import Monitoring from '~/monitoring/components/monitoring.vue'; +import { MonitorMockInterceptor } from './mock_data'; + +describe('Monitoring', () => { + const fixtureName = 'environments/metrics/metrics.html.raw'; + let MonitoringComponent; + let component; + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + MonitoringComponent = Vue.extend(Monitoring); + }); + + describe('no metrics are available yet', () => { + it('shows a getting started empty state when no metrics are present', () => { + component = new MonitoringComponent({ + el: document.querySelector('#prometheus-graphs'), + }); + + component.$mount(); + expect(component.$el.querySelector('#prometheus-graphs')).toBe(null); + expect(component.state).toEqual('gettingStarted'); + }); + }); + + describe('requests information to the server', () => { + beforeEach(() => { + document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true'); + Vue.http.interceptors.push(MonitorMockInterceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, MonitorMockInterceptor); + }); + + it('shows up a loading state', (done) => { + component = new MonitoringComponent({ + el: document.querySelector('#prometheus-graphs'), + }); + component.$mount(); + Vue.nextTick(() => { + expect(component.state).toEqual('loading'); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_state_spec.js b/spec/javascripts/monitoring/monitoring_state_spec.js new file mode 100644 index 00000000000..4c0c558502f --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_state_spec.js @@ -0,0 +1,110 @@ +import Vue from 'vue'; +import MonitoringState from '~/monitoring/components/monitoring_state.vue'; +import { statePaths } from './mock_data'; + +const createComponent = (propsData) => { + const Component = Vue.extend(MonitoringState); + + return new Component({ + propsData, + }).$mount(); +}; + +function getTextFromNode(component, selector) { + return component.$el.querySelector(selector).firstChild.nodeValue.trim(); +} + +describe('MonitoringState', () => { + describe('Computed props', () => { + it('currentState', () => { + const component = createComponent({ + selectedState: 'gettingStarted', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.currentState).toBe(component.states.gettingStarted); + }); + + it('buttonPath returns settings path for the state "gettingStarted"', () => { + const component = createComponent({ + selectedState: 'gettingStarted', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.buttonPath).toEqual(statePaths.settingsPath); + expect(component.buttonPath).not.toEqual(statePaths.documentationPath); + }); + + it('buttonPath returns documentation path for any of the other states', () => { + const component = createComponent({ + selectedState: 'loading', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.buttonPath).toEqual(statePaths.documentationPath); + expect(component.buttonPath).not.toEqual(statePaths.settingsPath); + }); + + it('showButtonDescription returns a description with a link for the unableToConnect state', () => { + const component = createComponent({ + selectedState: 'unableToConnect', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.showButtonDescription).toEqual(true); + }); + + it('showButtonDescription returns the description without a link for any other state', () => { + const component = createComponent({ + selectedState: 'loading', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.showButtonDescription).toEqual(false); + }); + }); + + it('should show the gettingStarted state', () => { + const component = createComponent({ + selectedState: 'gettingStarted', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.$el.querySelector('svg')).toBeDefined(); + expect(getTextFromNode(component, '.state-title')).toEqual(component.states.gettingStarted.title); + expect(getTextFromNode(component, '.state-description')).toEqual(component.states.gettingStarted.description); + expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.gettingStarted.buttonText); + }); + + it('should show the loading state', () => { + const component = createComponent({ + selectedState: 'loading', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.$el.querySelector('svg')).toBeDefined(); + expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title); + expect(getTextFromNode(component, '.state-description')).toEqual(component.states.loading.description); + expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText); + }); + + it('should show the unableToConnect state', () => { + const component = createComponent({ + selectedState: 'unableToConnect', + settingsPath: statePaths.settingsPath, + documentationPath: statePaths.documentationPath, + }); + + expect(component.$el.querySelector('svg')).toBeDefined(); + expect(getTextFromNode(component, '.state-title')).toEqual(component.states.unableToConnect.title); + expect(component.$el.querySelector('.state-description a')).toBeDefined(); + expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.unableToConnect.buttonText); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js new file mode 100644 index 00000000000..20c1e6a0005 --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_store_spec.js @@ -0,0 +1,24 @@ +import MonitoringStore from '~/monitoring/stores/monitoring_store'; +import MonitoringMock, { deploymentData } from './mock_data'; + +describe('MonitoringStore', () => { + this.store = new MonitoringStore(); + this.store.storeMetrics(MonitoringMock.data); + + it('contains one group that contains two queries sorted by priority in one row', () => { + expect(this.store.groups).toBeDefined(); + expect(this.store.groups.length).toEqual(1); + expect(this.store.groups[0].metrics.length).toEqual(1); + }); + + it('gets the metrics count for every group', () => { + expect(this.store.getMetricsCount()).toEqual(2); + }); + + it('contains deployment data', () => { + this.store.storeDeploymentData(deploymentData); + expect(this.store.deploymentData).toBeDefined(); + expect(this.store.deploymentData.length).toEqual(3); + expect(typeof this.store.deploymentData[0]).toEqual('object'); + }); +}); diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js deleted file mode 100644 index 25578bf1c6e..00000000000 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import 'jquery'; -import PrometheusGraph from '~/monitoring/prometheus_graph'; -import { prometheusMockData } from './prometheus_mock_data'; - -describe('PrometheusGraph', () => { - const fixtureName = 'environments/metrics/metrics.html.raw'; - const prometheusGraphContainer = '.prometheus-graph'; - const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`; - - preloadFixtures(fixtureName); - - beforeEach(() => { - loadFixtures(fixtureName); - $('.prometheus-container').data('has-metrics', 'true'); - this.prometheusGraph = new PrometheusGraph(); - const self = this; - const fakeInit = (metricsResponse) => { - self.prometheusGraph.transformData(metricsResponse); - self.prometheusGraph.createGraph(); - }; - spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit); - }); - - it('initializes graph properties', () => { - // Test for the measurements - expect(this.prometheusGraph.margin).toBeDefined(); - expect(this.prometheusGraph.marginLabelContainer).toBeDefined(); - expect(this.prometheusGraph.originalWidth).toBeDefined(); - expect(this.prometheusGraph.originalHeight).toBeDefined(); - expect(this.prometheusGraph.height).toBeDefined(); - expect(this.prometheusGraph.width).toBeDefined(); - expect(this.prometheusGraph.backOffRequestCounter).toBeDefined(); - // Test for the graph properties (colors, radius, etc.) - expect(this.prometheusGraph.graphSpecificProperties).toBeDefined(); - expect(this.prometheusGraph.commonGraphProperties).toBeDefined(); - }); - - it('transforms the data', () => { - this.prometheusGraph.init(prometheusMockData.metrics); - Object.keys(this.prometheusGraph.graphSpecificProperties, (key) => { - const graphProps = this.prometheusGraph.graphSpecificProperties[key]; - expect(graphProps.data).toBeDefined(); - expect(graphProps.data.length).toBe(121); - }); - }); - - it('creates two graphs', () => { - this.prometheusGraph.init(prometheusMockData.metrics); - expect($(prometheusGraphContainer).length).toBe(2); - }); - - describe('Graph contents', () => { - beforeEach(() => { - this.prometheusGraph.init(prometheusMockData.metrics); - }); - - it('has axis, an area, a line and a overlay', () => { - const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent(); - expect($graphContainer.find('.x-axis')).toBeDefined(); - expect($graphContainer.find('.y-axis')).toBeDefined(); - expect($graphContainer.find('.prometheus-graph-overlay')).toBeDefined(); - expect($graphContainer.find('.metric-line')).toBeDefined(); - expect($graphContainer.find('.metric-area')).toBeDefined(); - }); - - it('has legends, labels and an extra axis that labels the metrics', () => { - const $prometheusGraphContents = $(prometheusGraphContents); - const $axisLabelContainer = $(prometheusGraphContents).find('.label-x-axis-line').parent(); - expect($prometheusGraphContents.find('.label-x-axis-line')).toBeDefined(); - expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined(); - expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined(); - expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined(); - expect($axisLabelContainer.find('rect').length).toBe(3); - expect($axisLabelContainer.find('text').length).toBe(4); - }); - }); -}); - -describe('PrometheusGraphs UX states', () => { - const fixtureName = 'environments/metrics/metrics.html.raw'; - preloadFixtures(fixtureName); - - beforeEach(() => { - loadFixtures(fixtureName); - this.prometheusGraph = new PrometheusGraph(); - }); - - it('shows a specified state', () => { - this.prometheusGraph.state = '.js-getting-started'; - this.prometheusGraph.updateState(); - const $state = $('.js-getting-started'); - expect($state).toBeDefined(); - expect($('.state-title', $state)).toBeDefined(); - expect($('.state-svg', $state)).toBeDefined(); - expect($('.state-description', $state)).toBeDefined(); - expect($('.state-button', $state)).toBeDefined(); - }); -}); diff --git a/spec/javascripts/monitoring/prometheus_mock_data.js b/spec/javascripts/monitoring/prometheus_mock_data.js deleted file mode 100644 index 1cdc14faaa8..00000000000 --- a/spec/javascripts/monitoring/prometheus_mock_data.js +++ /dev/null @@ -1,1014 +0,0 @@ -/* eslint-disable import/prefer-default-export*/ -export const prometheusMockData = { - status: 200, - metrics: { - success: true, - metrics: { - memory_values: [ - { - metric: { - }, - values: [ - [ - 1488462917.256, - '10.12890625', - ], - [ - 1488462977.256, - '10.140625', - ], - [ - 1488463037.256, - '10.140625', - ], - [ - 1488463097.256, - '10.14453125', - ], - [ - 1488463157.256, - '10.1484375', - ], - [ - 1488463217.256, - '10.15625', - ], - [ - 1488463277.256, - '10.15625', - ], - [ - 1488463337.256, - '10.15625', - ], - [ - 1488463397.256, - '10.1640625', - ], - [ - 1488463457.256, - '10.171875', - ], - [ - 1488463517.256, - '10.171875', - ], - [ - 1488463577.256, - '10.171875', - ], - [ - 1488463637.256, - '10.18359375', - ], - [ - 1488463697.256, - '10.1953125', - ], - [ - 1488463757.256, - '10.203125', - ], - [ - 1488463817.256, - '10.20703125', - ], - [ - 1488463877.256, - '10.20703125', - ], - [ - 1488463937.256, - '10.20703125', - ], - [ - 1488463997.256, - '10.20703125', - ], - [ - 1488464057.256, - '10.2109375', - ], - [ - 1488464117.256, - '10.2109375', - ], - [ - 1488464177.256, - '10.2109375', - ], - [ - 1488464237.256, - '10.2109375', - ], - [ - 1488464297.256, - '10.21484375', - ], - [ - 1488464357.256, - '10.22265625', - ], - [ - 1488464417.256, - '10.22265625', - ], - [ - 1488464477.256, - '10.2265625', - ], - [ - 1488464537.256, - '10.23046875', - ], - [ - 1488464597.256, - '10.23046875', - ], - [ - 1488464657.256, - '10.234375', - ], - [ - 1488464717.256, - '10.234375', - ], - [ - 1488464777.256, - '10.234375', - ], - [ - 1488464837.256, - '10.234375', - ], - [ - 1488464897.256, - '10.234375', - ], - [ - 1488464957.256, - '10.234375', - ], - [ - 1488465017.256, - '10.23828125', - ], - [ - 1488465077.256, - '10.23828125', - ], - [ - 1488465137.256, - '10.2421875', - ], - [ - 1488465197.256, - '10.2421875', - ], - [ - 1488465257.256, - '10.2421875', - ], - [ - 1488465317.256, - '10.2421875', - ], - [ - 1488465377.256, - '10.2421875', - ], - [ - 1488465437.256, - '10.2421875', - ], - [ - 1488465497.256, - '10.2421875', - ], - [ - 1488465557.256, - '10.2421875', - ], - [ - 1488465617.256, - '10.2421875', - ], - [ - 1488465677.256, - '10.2421875', - ], - [ - 1488465737.256, - '10.2421875', - ], - [ - 1488465797.256, - '10.24609375', - ], - [ - 1488465857.256, - '10.25', - ], - [ - 1488465917.256, - '10.25390625', - ], - [ - 1488465977.256, - '9.98828125', - ], - [ - 1488466037.256, - '9.9921875', - ], - [ - 1488466097.256, - '9.9921875', - ], - [ - 1488466157.256, - '9.99609375', - ], - [ - 1488466217.256, - '10', - ], - [ - 1488466277.256, - '10.00390625', - ], - [ - 1488466337.256, - '10.0078125', - ], - [ - 1488466397.256, - '10.01171875', - ], - [ - 1488466457.256, - '10.0234375', - ], - [ - 1488466517.256, - '10.02734375', - ], - [ - 1488466577.256, - '10.02734375', - ], - [ - 1488466637.256, - '10.03125', - ], - [ - 1488466697.256, - '10.03125', - ], - [ - 1488466757.256, - '10.03125', - ], - [ - 1488466817.256, - '10.03125', - ], - [ - 1488466877.256, - '10.03125', - ], - [ - 1488466937.256, - '10.03125', - ], - [ - 1488466997.256, - '10.03125', - ], - [ - 1488467057.256, - '10.0390625', - ], - [ - 1488467117.256, - '10.0390625', - ], - [ - 1488467177.256, - '10.04296875', - ], - [ - 1488467237.256, - '10.05078125', - ], - [ - 1488467297.256, - '10.05859375', - ], - [ - 1488467357.256, - '10.06640625', - ], - [ - 1488467417.256, - '10.06640625', - ], - [ - 1488467477.256, - '10.0703125', - ], - [ - 1488467537.256, - '10.07421875', - ], - [ - 1488467597.256, - '10.0859375', - ], - [ - 1488467657.256, - '10.0859375', - ], - [ - 1488467717.256, - '10.09765625', - ], - [ - 1488467777.256, - '10.1015625', - ], - [ - 1488467837.256, - '10.10546875', - ], - [ - 1488467897.256, - '10.10546875', - ], - [ - 1488467957.256, - '10.125', - ], - [ - 1488468017.256, - '10.13671875', - ], - [ - 1488468077.256, - '10.1484375', - ], - [ - 1488468137.256, - '10.15625', - ], - [ - 1488468197.256, - '10.16796875', - ], - [ - 1488468257.256, - '10.171875', - ], - [ - 1488468317.256, - '10.171875', - ], - [ - 1488468377.256, - '10.171875', - ], - [ - 1488468437.256, - '10.171875', - ], - [ - 1488468497.256, - '10.171875', - ], - [ - 1488468557.256, - '10.171875', - ], - [ - 1488468617.256, - '10.171875', - ], - [ - 1488468677.256, - '10.17578125', - ], - [ - 1488468737.256, - '10.17578125', - ], - [ - 1488468797.256, - '10.265625', - ], - [ - 1488468857.256, - '10.19921875', - ], - [ - 1488468917.256, - '10.19921875', - ], - [ - 1488468977.256, - '10.19921875', - ], - [ - 1488469037.256, - '10.19921875', - ], - [ - 1488469097.256, - '10.19921875', - ], - [ - 1488469157.256, - '10.203125', - ], - [ - 1488469217.256, - '10.43359375', - ], - [ - 1488469277.256, - '10.20703125', - ], - [ - 1488469337.256, - '10.2109375', - ], - [ - 1488469397.256, - '10.22265625', - ], - [ - 1488469457.256, - '10.21484375', - ], - [ - 1488469517.256, - '10.21484375', - ], - [ - 1488469577.256, - '10.21484375', - ], - [ - 1488469637.256, - '10.22265625', - ], - [ - 1488469697.256, - '10.234375', - ], - [ - 1488469757.256, - '10.234375', - ], - [ - 1488469817.256, - '10.234375', - ], - [ - 1488469877.256, - '10.2421875', - ], - [ - 1488469937.256, - '10.25', - ], - [ - 1488469997.256, - '10.25390625', - ], - [ - 1488470057.256, - '10.26171875', - ], - [ - 1488470117.256, - '10.2734375', - ], - ], - }, - ], - memory_current: [ - { - metric: { - }, - value: [ - 1488470117.737, - '10.2734375', - ], - }, - ], - cpu_values: [ - { - metric: { - }, - values: [ - [ - 1488462918.15, - '0.0002996458625058103', - ], - [ - 1488462978.15, - '0.0002652382333333314', - ], - [ - 1488463038.15, - '0.0003485461333333421', - ], - [ - 1488463098.15, - '0.0003420421999999886', - ], - [ - 1488463158.15, - '0.00023107150000001297', - ], - [ - 1488463218.15, - '0.00030463981666664826', - ], - [ - 1488463278.15, - '0.0002477177833333677', - ], - [ - 1488463338.15, - '0.00026936656666665115', - ], - [ - 1488463398.15, - '0.000406264750000022', - ], - [ - 1488463458.15, - '0.00029592802026561453', - ], - [ - 1488463518.15, - '0.00023426999683316343', - ], - [ - 1488463578.15, - '0.0003057080666666915', - ], - [ - 1488463638.15, - '0.0003408470500000149', - ], - [ - 1488463698.15, - '0.00025497336666665166', - ], - [ - 1488463758.15, - '0.0003009282833333534', - ], - [ - 1488463818.15, - '0.0003119383499999924', - ], - [ - 1488463878.15, - '0.00028719019999998705', - ], - [ - 1488463938.15, - '0.000327864749999988', - ], - [ - 1488463998.15, - '0.0002514917333333422', - ], - [ - 1488464058.15, - '0.0003614651166666742', - ], - [ - 1488464118.15, - '0.0003221668000000122', - ], - [ - 1488464178.15, - '0.00023323083333330884', - ], - [ - 1488464238.15, - '0.00028531499475009274', - ], - [ - 1488464298.15, - '0.0002627695294921391', - ], - [ - 1488464358.15, - '0.00027145463333333453', - ], - [ - 1488464418.15, - '0.00025669488333335266', - ], - [ - 1488464478.15, - '0.00022307761666665965', - ], - [ - 1488464538.15, - '0.0003307265833333517', - ], - [ - 1488464598.15, - '0.0002817050666666709', - ], - [ - 1488464658.15, - '0.00022357458333332285', - ], - [ - 1488464718.15, - '0.00032648590000000275', - ], - [ - 1488464778.15, - '0.00028410750000000816', - ], - [ - 1488464838.15, - '0.0003038076999999954', - ], - [ - 1488464898.15, - '0.00037568226666667335', - ], - [ - 1488464958.15, - '0.00020160354999999202', - ], - [ - 1488465018.15, - '0.0003229403333333399', - ], - [ - 1488465078.15, - '0.00033516069999999236', - ], - [ - 1488465138.15, - '0.0003365978333333371', - ], - [ - 1488465198.15, - '0.00020262178333331585', - ], - [ - 1488465258.15, - '0.00040567498333331876', - ], - [ - 1488465318.15, - '0.00029114155000001436', - ], - [ - 1488465378.15, - '0.0002498841000000122', - ], - [ - 1488465438.15, - '0.00027296763333331715', - ], - [ - 1488465498.15, - '0.0002958794000000135', - ], - [ - 1488465558.15, - '0.0002922354666666867', - ], - [ - 1488465618.15, - '0.00034186624999999653', - ], - [ - 1488465678.15, - '0.0003397984166666627', - ], - [ - 1488465738.15, - '0.0002658284166666469', - ], - [ - 1488465798.15, - '0.00026221139999999346', - ], - [ - 1488465858.15, - '0.00029467960000001034', - ], - [ - 1488465918.15, - '0.0002634141333333358', - ], - [ - 1488465978.15, - '0.0003202958333333209', - ], - [ - 1488466038.15, - '0.00037890760000000394', - ], - [ - 1488466098.15, - '0.00023453356666666518', - ], - [ - 1488466158.15, - '0.0002866827333333433', - ], - [ - 1488466218.15, - '0.0003335935499999998', - ], - [ - 1488466278.15, - '0.00022787131666666125', - ], - [ - 1488466338.15, - '0.00033821938333333064', - ], - [ - 1488466398.15, - '0.00029233375000001043', - ], - [ - 1488466458.15, - '0.00026562758333333514', - ], - [ - 1488466518.15, - '0.0003142600999999819', - ], - [ - 1488466578.15, - '0.00027392178333333444', - ], - [ - 1488466638.15, - '0.00028178598333334173', - ], - [ - 1488466698.15, - '0.0002463400666666911', - ], - [ - 1488466758.15, - '0.00040234373333332125', - ], - [ - 1488466818.15, - '0.00023677453333332822', - ], - [ - 1488466878.15, - '0.00030852703333333523', - ], - [ - 1488466938.15, - '0.0003582272833333455', - ], - [ - 1488466998.15, - '0.0002176380833332973', - ], - [ - 1488467058.15, - '0.00026180203333335447', - ], - [ - 1488467118.15, - '0.00027862966666667436', - ], - [ - 1488467178.15, - '0.0002769731166666567', - ], - [ - 1488467238.15, - '0.0002832899166666477', - ], - [ - 1488467298.15, - '0.0003446533500000311', - ], - [ - 1488467358.15, - '0.0002691345999999761', - ], - [ - 1488467418.15, - '0.000284919933333357', - ], - [ - 1488467478.15, - '0.0002396026166666528', - ], - [ - 1488467538.15, - '0.00035625295000002075', - ], - [ - 1488467598.15, - '0.00036759816666664946', - ], - [ - 1488467658.15, - '0.00030326608333333855', - ], - [ - 1488467718.15, - '0.00023584972418043393', - ], - [ - 1488467778.15, - '0.00025744508892115107', - ], - [ - 1488467838.15, - '0.00036737541666663395', - ], - [ - 1488467898.15, - '0.00034325741666666094', - ], - [ - 1488467958.15, - '0.00026390046666667407', - ], - [ - 1488468018.15, - '0.0003302534500000102', - ], - [ - 1488468078.15, - '0.00035243794999999527', - ], - [ - 1488468138.15, - '0.00020149738333333407', - ], - [ - 1488468198.15, - '0.0003183469666666679', - ], - [ - 1488468258.15, - '0.0003835329166666845', - ], - [ - 1488468318.15, - '0.0002485075333333124', - ], - [ - 1488468378.15, - '0.0003011457166666768', - ], - [ - 1488468438.15, - '0.00032242785497684965', - ], - [ - 1488468498.15, - '0.0002659713747457531', - ], - [ - 1488468558.15, - '0.0003476860333333202', - ], - [ - 1488468618.15, - '0.00028336403333334794', - ], - [ - 1488468678.15, - '0.00017132354999998728', - ], - [ - 1488468738.15, - '0.0003001915833333276', - ], - [ - 1488468798.15, - '0.0003025715666666725', - ], - [ - 1488468858.15, - '0.0003012370166666815', - ], - [ - 1488468918.15, - '0.00030203619999997025', - ], - [ - 1488468978.15, - '0.0002804355000000314', - ], - [ - 1488469038.15, - '0.00033194884999998564', - ], - [ - 1488469098.15, - '0.00025201496666665455', - ], - [ - 1488469158.15, - '0.0002777531500000189', - ], - [ - 1488469218.15, - '0.0003314885833333392', - ], - [ - 1488469278.15, - '0.0002234891422095589', - ], - [ - 1488469338.15, - '0.000349117355867791', - ], - [ - 1488469398.15, - '0.0004036731333333303', - ], - [ - 1488469458.15, - '0.00024553911666667835', - ], - [ - 1488469518.15, - '0.0003056456833333184', - ], - [ - 1488469578.15, - '0.0002618737166666681', - ], - [ - 1488469638.15, - '0.00022972643333331414', - ], - [ - 1488469698.15, - '0.0003713522500000307', - ], - [ - 1488469758.15, - '0.00018322576666666515', - ], - [ - 1488469818.15, - '0.00034534762753952466', - ], - [ - 1488469878.15, - '0.00028200510008501677', - ], - [ - 1488469938.15, - '0.0002773708499999768', - ], - [ - 1488469998.15, - '0.00027547160000001013', - ], - [ - 1488470058.15, - '0.00031713610000000023', - ], - [ - 1488470118.15, - '0.00035276853333332525', - ], - ], - }, - ], - cpu_current: [ - { - metric: { - }, - value: [ - 1488470118.566, - '0.00035276853333332525', - ], - }, - ], - last_update: '2017-03-02T15:55:18.981Z', - }, - }, -}; diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js index 56c57d94798..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]'; 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/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 075b72f35b2..d4e134583c7 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -7,6 +7,10 @@ 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 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/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/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/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 5653cfee686..8813f129ef5 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 @@ -6,6 +6,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca before do allow(migration).to receive(:say) + TestEnv.clean_test_path end def migration_namespace(namespace) @@ -153,6 +154,30 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca end end + describe '#perform_rename' do + describe 'for namespaces' do + let(:namespace) { create(:namespace, path: 'the-path') } + it 'renames the path' do + subject.perform_rename(migration_namespace(namespace), 'the-path', 'renamed') + + expect(namespace.reload.path).to eq('renamed') + end + + it 'renames all the routes for the namespace' do + child = create(:group, path: 'child', parent: namespace) + project = create(:project, namespace: child, path: 'the-project') + other_one = create(:namespace, path: 'the-path-is-similar') + + subject.perform_rename(migration_namespace(namespace), 'the-path', 'renamed') + + expect(namespace.reload.route.path).to eq('renamed') + expect(child.reload.route.path).to eq('renamed/child') + expect(project.reload.route.path).to eq('renamed/child/the-project') + expect(other_one.reload.route.path).to eq('the-path-is-similar') + end + end + end + describe '#move_pages' do it 'moves the pages directory' do expect(subject).to receive(:move_folders) @@ -203,4 +228,53 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca expect(File.exist?(expected_file)).to be(true) end end + + describe '#track_rename', redis: true do + it 'tracks a rename in redis' do + key = 'rename:FakeRenameReservedPathMigrationV1:namespace' + + subject.track_rename('namespace', 'path/to/namespace', 'path/to/renamed') + + old_path, new_path = [nil, nil] + Gitlab::Redis.with do |redis| + rename_info = redis.lpop(key) + old_path, new_path = JSON.parse(rename_info) + end + + expect(old_path).to eq('path/to/namespace') + expect(new_path).to eq('path/to/renamed') + end + end + + describe '#reverts_for_type', redis: true do + it 'yields for each tracked rename' do + subject.track_rename('project', 'old_path', 'new_path') + subject.track_rename('project', 'old_path2', 'new_path2') + subject.track_rename('namespace', 'namespace_path', 'new_namespace_path') + + expect { |b| subject.reverts_for_type('project', &b) } + .to yield_successive_args(%w(old_path2 new_path2), %w(old_path new_path)) + expect { |b| subject.reverts_for_type('namespace', &b) } + .to yield_with_args('namespace_path', 'new_namespace_path') + end + + it 'keeps the revert in redis if it failed' do + subject.track_rename('project', 'old_path', 'new_path') + + subject.reverts_for_type('project') do + raise 'whatever happens, keep going!' + end + + key = 'rename:FakeRenameReservedPathMigrationV1:project' + stored_renames = nil + rename_count = 0 + Gitlab::Redis.with do |redis| + stored_renames = redis.lrange(key, 0, 1) + rename_count = redis.llen(key) + end + + expect(rename_count).to eq(1) + expect(JSON.parse(stored_renames.first)).to eq(%w(old_path new_path)) + end + end end 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 8125dedd3fc..803e923b4a5 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 @@ -3,9 +3,11 @@ require 'spec_helper' describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :truncate do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } + let(:namespace) { create(:group, name: 'the-path') } before do allow(migration).to receive(:say) + TestEnv.clean_test_path end def migration_namespace(namespace) @@ -137,8 +139,6 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : end describe "#rename_namespace" 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) @@ -149,11 +149,27 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : expect(namespace.reload.path).to eq('the-path0') end + it 'tracks the rename' do + expect(subject).to receive(:track_rename) + .with('namespace', 'the-path', 'the-path0') + + subject.rename_namespace(namespace) + end + + it 'renames things related to the namespace' do + expect(subject).to receive(:rename_namespace_dependencies) + .with(namespace, 'the-path', 'the-path0') + + subject.rename_namespace(namespace) + end + end + + describe '#rename_namespace_dependencies' do it "moves the the repository for a project in the namespace" do create(:project, namespace: namespace, path: "the-path-project") expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git") - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') expect(File.directory?(expected_repo)).to be(true) end @@ -161,13 +177,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : it "moves the uploads for the namespace" do expect(subject).to receive(:move_uploads).with("the-path", "the-path0") - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it "moves the pages for the namespace" do expect(subject).to receive(:move_pages).with("the-path", "the-path0") - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it 'invalidates the markdown cache of related projects' do @@ -175,13 +191,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : expect(subject).to receive(:remove_cached_html_for_projects).with([project.id]) - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it "doesn't rename users for other namespaces" do expect(subject).not_to receive(:rename_user) - subject.rename_namespace(namespace) + subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') end it 'renames the username of a namespace for a user' do @@ -189,7 +205,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : expect(subject).to receive(:rename_user).with('the-path', 'the-path0') - subject.rename_namespace(user.namespace) + subject.rename_namespace_dependencies(user.namespace, 'the-path', 'the-path0') end end @@ -224,4 +240,50 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : subject.rename_namespaces(type: :child) end end + + describe '#revert_renames', redis: true do + it 'renames the routes back to the previous values' do + project = create(:project, path: 'a-project', namespace: namespace) + subject.rename_namespace(namespace) + + expect(subject).to receive(:perform_rename) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace), + 'the-path0', + 'the-path' + ).and_call_original + + subject.revert_renames + + expect(namespace.reload.path).to eq('the-path') + expect(namespace.reload.route.path).to eq('the-path') + expect(project.reload.route.path).to eq('the-path/a-project') + end + + it 'moves the repositories back to their original place' do + project = create(:project, path: 'a-project', namespace: namespace) + project.create_repository + subject.rename_namespace(namespace) + + expected_path = File.join(TestEnv.repos_path, 'the-path', 'a-project.git') + + expect(subject).to receive(:rename_namespace_dependencies) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace), + 'the-path0', + 'the-path' + ).and_call_original + + subject.revert_renames + + expect(File.directory?(expected_path)).to be_truthy + end + + it "doesn't break when the namespace was renamed" do + subject.rename_namespace(namespace) + namespace.update_attributes!(path: 'renamed-afterwards') + + expect { subject.revert_renames }.not_to raise_error + end + end 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 802f77ad430..0e240a5ccf1 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 @@ -3,9 +3,15 @@ require 'spec_helper' describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :truncate do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } + let(:project) do + create(:empty_project, + path: 'the-path', + namespace: create(:namespace, path: 'known-parent' )) + end before do allow(migration).to receive(:say) + TestEnv.clean_test_path end describe '#projects_for_paths' do @@ -47,12 +53,6 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr end describe '#rename_project' do - let(:project) do - create(:empty_project, - path: 'the-path', - namespace: create(:namespace, path: 'known-parent' )) - end - it 'renames path & route for the project' do expect(subject).to receive(:rename_path_for_routable) .with(project) @@ -63,27 +63,42 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr expect(project.reload.path).to eq('the-path0') end + it 'tracks the rename' do + expect(subject).to receive(:track_rename) + .with('project', 'known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + + it 'renames the folders for the project' do + expect(subject).to receive(:move_project_folders).with(project, 'known-parent/the-path', 'known-parent/the-path0') + + subject.rename_project(project) + end + end + + describe '#move_project_folders' do 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') - subject.rename_project(project) + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end it 'moves uploads' do expect(subject).to receive(:move_uploads) .with('known-parent/the-path', 'known-parent/the-path0') - subject.rename_project(project) + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end it 'moves pages' do expect(subject).to receive(:move_pages) .with('known-parent/the-path', 'known-parent/the-path0') - subject.rename_project(project) + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end end @@ -99,4 +114,47 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr expect(File.directory?(expected_path)).to be(true) end end + + describe '#revert_renames', redis: true do + it 'renames the routes back to the previous values' do + subject.rename_project(project) + + expect(subject).to receive(:perform_rename) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project), + 'known-parent/the-path0', + 'known-parent/the-path' + ).and_call_original + + subject.revert_renames + + expect(project.reload.path).to eq('the-path') + expect(project.route.path).to eq('known-parent/the-path') + end + + it 'moves the repositories back to their original place' do + project.create_repository + subject.rename_project(project) + + expected_path = File.join(TestEnv.repos_path, 'known-parent', 'the-path.git') + + expect(subject).to receive(:move_project_folders) + .with( + kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project), + 'known-parent/the-path0', + 'known-parent/the-path' + ).and_call_original + + subject.revert_renames + + expect(File.directory?(expected_path)).to be_truthy + end + + it "doesn't break when the project was renamed" do + subject.rename_project(project) + project.update_attributes!(path: 'renamed-afterwards') + + expect { subject.revert_renames }.not_to raise_error + end + end 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 1d5e58855c1..7695b95dc57 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 @@ -51,4 +51,26 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :truncate do subject.rename_root_paths('the-path') end end + + describe '#revert_renames' do + it 'renames namespaces' do + rename_namespaces = double + expect(described_class::RenameNamespaces) + .to receive(:new).with([], subject) + .and_return(rename_namespaces) + expect(rename_namespaces).to receive(:revert_renames) + + subject.revert_renames + end + + it 'renames projects' do + rename_projects = double + expect(described_class::RenameProjects) + .to receive(:new).with([], subject) + .and_return(rename_projects) + expect(rename_projects).to receive(:revert_renames) + + subject.revert_renames + end + end end diff --git a/spec/lib/gitlab/database/sha_attribute_spec.rb b/spec/lib/gitlab/database/sha_attribute_spec.rb new file mode 100644 index 00000000000..62c1d37ea1c --- /dev/null +++ b/spec/lib/gitlab/database/sha_attribute_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Gitlab::Database::ShaAttribute do + let(:sha) do + '9a573a369a5bfbb9a4a36e98852c21af8a44ea8b' + end + + let(:binary_sha) do + [sha].pack('H*') + end + + let(:binary_from_db) do + if Gitlab::Database.postgresql? + "\\x#{sha}" + else + binary_sha + end + end + + let(:attribute) { described_class.new } + + describe '#type_cast_from_database' do + it 'converts the binary SHA to a String' do + expect(attribute.type_cast_from_database(binary_from_db)).to eq(sha) + end + end + + describe '#type_cast_for_database' do + it 'converts a SHA String to binary data' do + expect(attribute.type_cast_for_database(sha).to_s).to eq(binary_sha) + end + end +end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 5e6206b96c7..cbf6c35356e 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -176,6 +176,10 @@ describe Gitlab::Database, lib: true do 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 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 7e32770f95d..64b233f3e68 100644 --- a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb @@ -87,5 +87,9 @@ describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do it 'links URLs' do expect(subject).to include(link('http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl', 'http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl')) end + + it 'does not contain link with a newline as package name' do + expect(subject).not_to include(link("\n", "https://pypi.python.org/pypi/\n")) + end 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/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/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index 3f279c21865..73518656bde 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -4,18 +4,20 @@ require 'fileutils' describe Gitlab::Git::Hook, lib: true do describe "#trigger" do let(:project) { create(:project, :repository) } + let(:repo_path) { project.repository.path } let(:user) { create(:user) } + let(:gl_id) { Gitlab::GlId.gl_id(user) } def create_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write('exit 0') end end def create_failing_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write(<<-HOOK) echo 'regular message from the hook' echo 'error message from the hook' 1>&2 @@ -27,13 +29,29 @@ describe Gitlab::Git::Hook, lib: true do ['pre-receive', 'post-receive', 'update'].each do |hook_name| context "when triggering a #{hook_name} hook" do context "when the hook is successful" do + let(:hook_path) { File.join(repo_path, 'hooks', hook_name) } + let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) } + let(:env) do + { + 'GL_ID' => gl_id, + 'PWD' => repo_path, + 'GL_PROTOCOL' => 'web', + 'GL_REPOSITORY' => gl_repository + } + end + it "returns success with no errors" do create_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + if hook_name != 'update' + expect(Open3).to receive(:popen3) + .with(env, hook_path, chdir: repo_path).and_call_original + end + + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_blank end @@ -42,11 +60,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook is unsuccessful" do it "returns failure with errors" do create_failing_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be false expect(errors).to eq("error message from the hook\n") end @@ -56,11 +74,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook doesn't exist" do it "returns success with no errors" do - hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path) + hook = Gitlab::Git::Hook.new('unknown_hook', project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_nil end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 703b0c2c202..0cd458bf933 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -26,6 +26,10 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + it 'returns UTF-8' do + expect(repository.root_ref.encoding).to eq(Encoding.find('UTF-8')) + end + context 'with gitaly enabled' do before do stub_gitaly @@ -123,6 +127,11 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'has SeedRepo::Repo::BRANCHES.size elements' do expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size) end + + it 'returns UTF-8' do + expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + end + it { is_expected.to include("master") } it { is_expected.not_to include("branch-from-space") } @@ -158,10 +167,15 @@ describe Gitlab::Git::Repository, seed_helper: true do subject { repository.tag_names } it { is_expected.to be_kind_of Array } + it 'has SeedRepo::Repo::TAGS.size elements' do expect(subject.size).to eq(SeedRepo::Repo::TAGS.size) end + it 'returns UTF-8' do + expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + end + describe '#last' do subject { super().last } it { is_expected.to eq("v1.2.1") } @@ -348,7 +362,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 } @@ -383,12 +397,12 @@ 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", { @@ -414,7 +428,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 @@ -1101,35 +1115,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() @@ -1305,6 +1290,16 @@ describe Gitlab::Git::Repository, seed_helper: true do Gitlab::GitalyClient.clear_stubs! end + it 'returns a Branch with UTF-8 fields' do + branches = @repo.local_branches.to_a + expect(branches.size).to be > 0 + utf_8 = Encoding.find('utf-8') + branches.each do |branch| + expect(branch.name.encoding).to eq(utf_8) + expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil? + end + end + it 'gets the branches from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) .and_return([]) diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index 42dba2ff874..8ad39a02b93 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -69,6 +69,15 @@ describe Gitlab::GitalyClient::Ref do client.local_branches(sort_by: 'updated_desc') end + it 'translates known mismatches on sort param values' do + expect_any_instance_of(Gitaly::Ref::Stub) + .to receive(:find_local_branches) + .with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash)) + .and_return([]) + + client.local_branches(sort_by: 'name_asc') + end + it 'raises an argument error if an invalid sort_by parameter is passed' do expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError) end 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/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 168a59e5139..30b6a0d8845 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::ImportExport::RepoRestorer, services: true do it 'has the webhooks' do restorer.restore - expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist + expect(Gitlab::Git::Hook.new('post-receive', project)).to exist end end end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 9dd997aa7dc..756fcb0fcaf 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -4,6 +4,16 @@ describe Gitlab::LDAP::Access, lib: true do let(:access) { Gitlab::LDAP::Access.new user } let(:user) { create(:omniauth_user) } + describe '.allowed?' do + it 'updates the users `last_credential_check_at' do + expect(access).to receive(:allowed?) { true } + expect(described_class).to receive(:open).and_yield(access) + + expect { described_class.allowed?(user) } + .to change { user.last_credential_check_at } + end + end + describe '#allowed?' do subject { access.allowed? } diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index b47e1b56fa9..c6718827028 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -30,13 +30,15 @@ describe Gitlab::UsageData do expect(count_data.keys).to match_array(%i( boards ci_builds - ci_pipelines + ci_internal_pipelines + ci_external_pipelines ci_runners ci_triggers ci_pipeline_schedules deploy_keys deployments environments + in_review_folder groups issues keys 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/models/ability_spec.rb b/spec/models/ability_spec.rb index 090f9e70c50..dc7a0d80752 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Ability, lib: true do context 'using a nil subject' do - it 'is always empty' do - expect(Ability.allowed(nil, nil).to_set).to be_empty + it 'has no permissions' do + expect(Ability.policy_for(nil, nil)).to be_banned end end @@ -255,12 +255,15 @@ describe Ability, lib: true do describe '.project_disabled_features_rules' do let(:project) { create(:empty_project, :wiki_disabled) } - subject { described_class.allowed(project.owner, project) } + subject { described_class.policy_for(project.owner, project) } context 'wiki named abilities' do it 'disables wiki abilities if the project has no wiki' do expect(project).to receive(:has_external_wiki?).and_return(false) - expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) + expect(subject).not_to be_allowed(:read_wiki) + expect(subject).not_to be_allowed(:create_wiki) + expect(subject).not_to be_allowed(:update_wiki) + expect(subject).not_to be_allowed(:admin_wiki) end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index dab8e8ca432..55d85a6e228 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -672,6 +672,12 @@ describe Ci::Pipeline, models: true do end end + describe '.internal_sources' do + subject { described_class.internal_sources } + + it { is_expected.to be_an(Array) } + end + describe '#status' do let(:build) do create(:ci_build, :created, pipeline: pipeline, name: 'test') diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 83494af24ba..329682a0771 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -3,14 +3,8 @@ require 'spec_helper' describe Ci::Variable, models: true do subject { build(:ci_variable) } - let(:secret_value) { 'secret' } - - it { is_expected.to validate_presence_of(:key) } + it { is_expected.to include_module(HasVariable) } it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) } - it { is_expected.to validate_length_of(:key).is_at_most(255) } - it { is_expected.to allow_value('foo').for(:key) } - it { is_expected.not_to allow_value('foo bar').for(:key) } - it { is_expected.not_to allow_value('foo/bar').for(:key) } describe '.unprotected' do subject { described_class.unprotected } @@ -33,36 +27,4 @@ describe Ci::Variable, models: true do end end end - - describe '#value' do - before do - subject.value = secret_value - end - - it 'stores the encrypted value' do - expect(subject.encrypted_value).not_to be_nil - end - - it 'stores an iv for value' do - expect(subject.encrypted_value_iv).not_to be_nil - end - - it 'stores a salt for value' do - expect(subject.encrypted_value_salt).not_to be_nil - end - - 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') - end - end - - describe '#to_runner_variable' do - it 'returns a hash for the runner' do - expect(subject.to_runner_variable) - .to eq(key: subject.key, value: subject.value, public: false) - end - end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 9262ce08987..1e074c7ad26 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -284,6 +284,41 @@ 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 } diff --git a/spec/models/concerns/feature_gate_spec.rb b/spec/models/concerns/feature_gate_spec.rb new file mode 100644 index 00000000000..3f601243245 --- /dev/null +++ b/spec/models/concerns/feature_gate_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe FeatureGate do + describe 'User' do + describe '#flipper_id' do + context 'when user is not persisted' do + let(:user) { build(:user) } + + it { expect(user.flipper_id).to be_nil } + end + + context 'when user is persisted' do + let(:user) { create(:user) } + + it { expect(user.flipper_id).to eq "User:#{user.id}" } + end + end + end +end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 101567998c9..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 diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb new file mode 100644 index 00000000000..f4b24e6d1d9 --- /dev/null +++ b/spec/models/concerns/has_variable_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe HasVariable do + subject { build(:ci_variable) } + + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_length_of(:key).is_at_most(255) } + it { is_expected.to allow_value('foo').for(:key) } + it { is_expected.not_to allow_value('foo bar').for(:key) } + it { is_expected.not_to allow_value('foo/bar').for(:key) } + + describe '#value' do + before do + subject.value = 'secret' + end + + it 'stores the encrypted value' do + expect(subject.encrypted_value).not_to be_nil + end + + it 'stores an iv for value' do + expect(subject.encrypted_value_iv).not_to be_nil + end + + it 'stores a salt for value' do + expect(subject.encrypted_value_salt).not_to be_nil + end + + 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') + end + end + + describe '#to_runner_variable' do + it 'returns a hash for the runner' do + expect(subject.to_runner_variable) + .to eq(key: subject.key, value: subject.value, public: false) + end + end +end diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb new file mode 100644 index 00000000000..9e37c2b20c4 --- /dev/null +++ b/spec/models/concerns/sha_attribute_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe ShaAttribute do + let(:model) { Class.new { include ShaAttribute } } + + before do + columns = [ + double(:column, name: 'name', type: :text), + double(:column, name: 'sha1', type: :binary) + ] + + allow(model).to receive(:columns).and_return(columns) + end + + describe '#sha_attribute' do + it 'defines a SHA attribute for a binary column' do + expect(model).to receive(:attribute) + .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute)) + + model.sha_attribute(:sha1) + end + + it 'raises ArgumentError when the column type is not :binary' do + expect { model.sha_attribute(:name) }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1240c9745e2..bb5273074a2 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1574,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/namespace_spec.rb b/spec/models/namespace_spec.rb index e7c3acf19eb..62c4ea01ce1 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -323,6 +323,36 @@ describe Namespace, models: true do end end + describe '#users_with_descendants', :nested_groups do + let(:user_a) { create(:user) } + let(:user_b) { create(:user) } + + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + + it 'returns member users on every nest level without duplication' do + group.add_developer(user_a) + nested_group.add_developer(user_b) + deep_nested_group.add_developer(user_a) + + expect(group.users_with_descendants).to contain_exactly(user_a, user_b) + expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b) + expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a) + end + end + + describe '#soft_delete_without_removing_associations' do + let(:project1) { create(:project_empty_repo, namespace: namespace) } + + it 'updates the deleted_at timestamp but preserves projects' do + namespace.soft_delete_without_removing_associations + + expect(Project.all).to include(project1) + expect(namespace.deleted_at).not_to be_nil + end + end + 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) diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index e62fd69e567..7b1a554d1fb 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -217,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/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb index de55627dd27..56ff3596190 100644 --- a/spec/models/project_services/campfire_service_spec.rb +++ b/spec/models/project_services/campfire_service_spec.rb @@ -39,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) @@ -66,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/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index e2b8226124f..c86f56c55eb 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -106,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' - - 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) + 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).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 @@ -202,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/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index c914b32f7a4..37f23b1243c 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -71,7 +71,7 @@ describe PrometheusService, models: true, caching: true do 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| @@ -80,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_metrics_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 diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index 7349eb4149a..6b004098510 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -205,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 d7fcadb895e..1390848ff4a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -284,15 +284,6 @@ describe Project, models: true do end end - describe 'default_scope' do - it 'excludes projects pending deletion from the results' do - project = create(:empty_project) - create(:empty_project, pending_delete: true) - - expect(Project.all).to eq [project] - end - end - describe 'project token' do it 'sets an random token if none provided' do project = FactoryGirl.create :empty_project, runners_token: '' @@ -1179,6 +1170,16 @@ describe Project, models: true do expect(relation.search(project.namespace.name)).to eq([project]) end + + describe 'with pending_delete project' do + let(:pending_delete_project) { create(:empty_project, pending_delete: true) } + + it 'shows pending deletion project' do + search_result = described_class.search(pending_delete_project.name) + + expect(search_result).to eq([pending_delete_project]) + end + end end describe '#rename_repo' do @@ -1327,6 +1328,37 @@ describe Project, models: true do end end + describe '#ensure_repository' do + let(:project) { create(:project, :repository) } + let(:shell) { Gitlab::Shell.new } + + before do + allow(project).to receive(:gitlab_shell).and_return(shell) + end + + it 'creates the repository if it not exist' do + allow(project).to receive(:repository_exists?) + .and_return(false) + + allow(shell).to receive(:add_repository) + .with(project.repository_storage_path, project.path_with_namespace) + .and_return(true) + + expect(project).to receive(:create_repository) + + project.ensure_repository + end + + it 'does not create the repository if it exists' do + allow(project).to receive(:repository_exists?) + .and_return(true) + + expect(project).not_to receive(:create_repository) + + project.ensure_repository + end + end + describe '#user_can_push_to_empty_repo?' do let(:project) { create(:empty_project) } let(:user) { create(:user) } @@ -1478,6 +1510,40 @@ describe Project, models: true do end end + describe 'project import state transitions' do + context 'state transition: [:started] => [:finished]' do + let(:housekeeping_service) { spy } + + before do + allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service } + end + + it 'performs housekeeping when an import of a fresh project is completed' do + project = create(:project_empty_repo, :import_started, import_type: :github) + + project.import_finish + + expect(housekeeping_service).to have_received(:execute) + end + + it 'does not perform housekeeping when project repository does not exist' do + project = create(:empty_project, :import_started, import_type: :github) + + project.import_finish + + expect(housekeeping_service).not_to have_received(:execute) + end + + it 'does not perform housekeeping when project does not have a valid import type' do + project = create(:empty_project, :import_started, import_type: nil) + + project.import_finish + + expect(housekeeping_service).not_to have_received(:execute) + end + end + end + describe '#latest_successful_builds_for' do def create_pipeline(status = 'success') create(:ci_pipeline, project: project, diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index bf74ac5ea25..1f314791479 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -278,6 +278,24 @@ describe ProjectWiki, models: true do end end + describe '#ensure_repository' do + it 'creates the repository if it not exist' do + allow(subject).to receive(:repository_exists?).and_return(false) + + expect(subject).to receive(:create_repo!) + + subject.ensure_repository + end + + it 'does not create the repository if it exists' do + allow(subject).to receive(:repository_exists?).and_return(true) + + expect(subject).not_to receive(:create_repo!) + + subject.ensure_repository + end + end + describe '#hook_attrs' do it 'returns a hash with values' do expect(subject.hook_attrs).to be_a Hash diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3e984ec7588..c69f0a495db 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -780,7 +780,7 @@ 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') + .with(user, project, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -823,12 +823,7 @@ describe Repository, models: true do service = GitHooksService.new expect(GitHooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with( - user, - repository.path_to_repo, - old_rev, - new_rev, - 'refs/heads/feature') + .with(user, project, old_rev, new_rev, 'refs/heads/feature') .and_yield(service).and_return(true) end @@ -1474,9 +1469,9 @@ describe Repository, models: true do it 'passes commit SHA to pre-receive and update hooks,\ and tag SHA to post-receive hook' do - pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo) - update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo) - post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo) + pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project) + update_hook = Gitlab::Git::Hook.new('update', project) + post_receive_hook = Gitlab::Git::Hook.new('post-receive', project) allow(Gitlab::Git::Hook).to receive(:new) .and_return(pre_receive_hook, update_hook, post_receive_hook) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8e895ec6634..448555d2190 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -754,42 +754,49 @@ describe User, models: true do end describe '.search' do - let(:user) { create(:user) } + let!(:user) { create(:user, name: 'user', username: 'usern', email: 'email@gmail.com') } + let!(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@gmail.com') } - it 'returns users with a matching name' do - expect(described_class.search(user.name)).to eq([user]) - end + describe 'name matching' do + it 'returns users with a matching name with exact match first' do + expect(described_class.search(user.name)).to eq([user, user2]) + end - it 'returns users with a partially matching name' do - expect(described_class.search(user.name[0..2])).to eq([user]) - end + it 'returns users with a partially matching name' do + expect(described_class.search(user.name[0..2])).to eq([user2, user]) + end - it 'returns users with a matching name regardless of the casing' do - expect(described_class.search(user.name.upcase)).to eq([user]) + it 'returns users with a matching name regardless of the casing' do + expect(described_class.search(user2.name.upcase)).to eq([user2]) + end end - it 'returns users with a matching Email' do - expect(described_class.search(user.email)).to eq([user]) - end + describe 'email matching' do + it 'returns users with a matching Email' do + expect(described_class.search(user.email)).to eq([user, user2]) + end - it 'returns users with a partially matching Email' do - expect(described_class.search(user.email[0..2])).to eq([user]) - end + it 'returns users with a partially matching Email' do + expect(described_class.search(user.email[0..2])).to eq([user2, user]) + end - it 'returns users with a matching Email regardless of the casing' do - expect(described_class.search(user.email.upcase)).to eq([user]) + it 'returns users with a matching Email regardless of the casing' do + expect(described_class.search(user2.email.upcase)).to eq([user2]) + end end - it 'returns users with a matching username' do - expect(described_class.search(user.username)).to eq([user]) - end + describe 'username matching' do + it 'returns users with a matching username' do + expect(described_class.search(user.username)).to eq([user, user2]) + end - it 'returns users with a partially matching username' do - expect(described_class.search(user.username[0..2])).to eq([user]) - end + it 'returns users with a partially matching username' do + expect(described_class.search(user.username[0..2])).to eq([user2, user]) + end - it 'returns users with a matching username regardless of the casing' do - expect(described_class.search(user.username.upcase)).to eq([user]) + it 'returns users with a matching username regardless of the casing' do + expect(described_class.search(user2.username.upcase)).to eq([user2]) + end end end diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index 02acdcb36df..e1963091a72 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe BasePolicy, models: true do describe '.class_for' do it 'detects policy class based on the subject ancestors' do - expect(described_class.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy) + expect(DeclarativePolicy.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy) end it 'detects policy class for a presented subject' do presentee = Ci::BuildPresenter.new(Ci::Build.new) - expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy) + expect(DeclarativePolicy.class_for(presentee)).to eq(Ci::BuildPolicy) end it 'uses GlobalPolicy when :global is given' do - expect(described_class.class_for(:global)).to eq(GlobalPolicy) + expect(DeclarativePolicy.class_for(:global)).to eq(GlobalPolicy) end end end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 48a139d4b83..ace95ac7067 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -5,8 +5,8 @@ describe Ci::BuildPolicy, :models do let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } - let(:policies) do - described_class.abilities(user, build).to_set + let(:policy) do + described_class.new(user, build) end shared_context 'public pipelines disabled' do @@ -21,7 +21,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end @@ -29,7 +29,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end end @@ -39,7 +39,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'includes ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end @@ -47,7 +47,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end end @@ -62,7 +62,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'includes ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end @@ -70,7 +70,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).not_to include :read_build + expect(policy).not_to be_allowed :read_build end end end @@ -82,7 +82,7 @@ describe Ci::BuildPolicy, :models do context 'when public builds are enabled' do it 'includes ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end @@ -90,7 +90,7 @@ describe Ci::BuildPolicy, :models do include_context 'public pipelines disabled' it 'does not include ability to read build' do - expect(policies).to include :read_build + expect(policy).to be_allowed :read_build end end end @@ -115,7 +115,7 @@ describe Ci::BuildPolicy, :models do end it 'does not include ability to update build' do - expect(policies).not_to include :update_build + expect(policy).to be_disallowed :update_build end end @@ -125,7 +125,7 @@ describe Ci::BuildPolicy, :models do end it 'includes ability to update build' do - expect(policies).to include :update_build + expect(policy).to be_allowed :update_build end end end @@ -135,7 +135,7 @@ describe Ci::BuildPolicy, :models do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } it 'includes ability to update build' do - expect(policies).to include :update_build + expect(policy).to be_allowed :update_build end end @@ -143,7 +143,7 @@ describe Ci::BuildPolicy, :models do let(:build) { create(:ci_build, pipeline: pipeline) } it 'includes ability to update build' do - expect(policies).to include :update_build + expect(policy).to be_allowed :update_build end end end diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb index 63ad5eb7322..ed4010e723b 100644 --- a/spec/policies/ci/trigger_policy_spec.rb +++ b/spec/policies/ci/trigger_policy_spec.rb @@ -6,36 +6,36 @@ describe Ci::TriggerPolicy, :models do let(:trigger) { create(:ci_trigger, project: project, owner: owner) } let(:policies) do - described_class.abilities(user, trigger).to_set + described_class.new(user, trigger) end shared_examples 'allows to admin and manage trigger' do it 'does include ability to admin trigger' do - expect(policies).to include :admin_trigger + expect(policies).to be_allowed :admin_trigger end it 'does include ability to manage trigger' do - expect(policies).to include :manage_trigger + expect(policies).to be_allowed :manage_trigger end end shared_examples 'allows to manage trigger' do it 'does not include ability to admin trigger' do - expect(policies).not_to include :admin_trigger + expect(policies).not_to be_allowed :admin_trigger end it 'does include ability to manage trigger' do - expect(policies).to include :manage_trigger + expect(policies).to be_allowed :manage_trigger end end shared_examples 'disallows to admin and manage trigger' do it 'does not include ability to admin trigger' do - expect(policies).not_to include :admin_trigger + expect(policies).not_to be_allowed :admin_trigger end it 'does not include ability to manage trigger' do - expect(policies).not_to include :manage_trigger + expect(policies).not_to be_allowed :manage_trigger end end diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb index 28e10f0bfe2..f15f4a11f02 100644 --- a/spec/policies/deploy_key_policy_spec.rb +++ b/spec/policies/deploy_key_policy_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe DeployKeyPolicy, models: true do - subject { described_class.abilities(current_user, deploy_key).to_set } + subject { described_class.new(current_user, deploy_key) } describe 'updating a deploy_key' do context 'when a regular user' do @@ -16,7 +16,7 @@ describe DeployKeyPolicy, models: true do project.deploy_keys << deploy_key end - it { is_expected.to include(:update_deploy_key) } + it { is_expected.to be_allowed(:update_deploy_key) } end context 'tries to update private deploy key attached to other project' do @@ -27,13 +27,13 @@ describe DeployKeyPolicy, models: true do other_project.deploy_keys << deploy_key end - it { is_expected.not_to include(:update_deploy_key) } + it { is_expected.to be_disallowed(:update_deploy_key) } end context 'tries to update public deploy key' do let(:deploy_key) { create(:another_deploy_key, public: true) } - it { is_expected.not_to include(:update_deploy_key) } + it { is_expected.to be_disallowed(:update_deploy_key) } end end @@ -43,13 +43,13 @@ describe DeployKeyPolicy, models: true do context ' tries to update private deploy key' do let(:deploy_key) { create(:deploy_key, public: false) } - it { is_expected.to include(:update_deploy_key) } + it { is_expected.to be_allowed(:update_deploy_key) } end context 'when an admin user tries to update public deploy key' do let(:deploy_key) { create(:another_deploy_key, public: true) } - it { is_expected.to include(:update_deploy_key) } + it { is_expected.to be_allowed(:update_deploy_key) } end end end diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb index 650432520bb..035e20c7452 100644 --- a/spec/policies/environment_policy_spec.rb +++ b/spec/policies/environment_policy_spec.rb @@ -8,8 +8,8 @@ describe EnvironmentPolicy do create(:environment, :with_review_app, project: project) end - let(:policies) do - described_class.abilities(user, environment).to_set + let(:policy) do + described_class.new(user, environment) end describe '#rules' do @@ -17,7 +17,7 @@ describe EnvironmentPolicy do let(:project) { create(:project, :private) } it 'does not include ability to stop environment' do - expect(policies).not_to include :stop_environment + expect(policy).to be_disallowed :stop_environment end end @@ -25,7 +25,7 @@ describe EnvironmentPolicy do let(:project) { create(:project, :public) } it 'does not include ability to stop environment' do - expect(policies).not_to include :stop_environment + expect(policy).to be_disallowed :stop_environment end end @@ -38,7 +38,7 @@ describe EnvironmentPolicy do context 'when team member has ability to stop environment' do it 'does includes ability to stop environment' do - expect(policies).to include :stop_environment + expect(policy).to be_allowed :stop_environment end end @@ -49,7 +49,7 @@ describe EnvironmentPolicy do end it 'does not include ability to stop environment' do - expect(policies).not_to include :stop_environment + expect(policy).to be_disallowed :stop_environment end end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index a8331ceb5ff..06db0ea56e3 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -36,16 +36,24 @@ describe GroupPolicy, models: true do group.add_owner(owner) end - subject { described_class.abilities(current_user, group).to_set } + subject { described_class.new(current_user, group) } + + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end context 'with no user' do let(:current_user) { nil } it do - is_expected.to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -53,10 +61,10 @@ describe GroupPolicy, models: true do let(:current_user) { guest } it do - is_expected.to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -64,10 +72,10 @@ describe GroupPolicy, models: true do let(:current_user) { reporter } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -75,10 +83,10 @@ describe GroupPolicy, models: true do let(:current_user) { developer } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -86,10 +94,10 @@ describe GroupPolicy, models: true do let(:current_user) { master } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -97,10 +105,10 @@ describe GroupPolicy, models: true do let(:current_user) { owner } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end @@ -108,10 +116,10 @@ describe GroupPolicy, models: true do let(:current_user) { admin } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end @@ -130,16 +138,16 @@ describe GroupPolicy, models: true do nested_group.add_owner(owner) end - subject { described_class.abilities(current_user, nested_group).to_set } + subject { described_class.new(current_user, nested_group) } context 'with no user' do let(:current_user) { nil } it do - is_expected.not_to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_disallowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -147,10 +155,10 @@ describe GroupPolicy, models: true do let(:current_user) { guest } it do - is_expected.to include(:read_group) - is_expected.not_to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_disallowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -158,10 +166,10 @@ describe GroupPolicy, models: true do let(:current_user) { reporter } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -169,10 +177,10 @@ describe GroupPolicy, models: true do let(:current_user) { developer } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -180,10 +188,10 @@ describe GroupPolicy, models: true do let(:current_user) { master } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -191,10 +199,10 @@ describe GroupPolicy, models: true do let(:current_user) { owner } it do - is_expected.to include(:read_group) - is_expected.to include(*reporter_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(:read_group) + expect_allowed(*reporter_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end end diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index 4a07c864428..c978cbd6185 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -9,7 +9,7 @@ describe IssuePolicy, models: true do let(:reporter_from_group_link) { create(:user) } def permissions(user, issue) - described_class.abilities(user, issue).to_set + described_class.new(user, issue) end context 'a private project' do @@ -30,42 +30,42 @@ describe IssuePolicy, models: true do end it 'does not allow non-members to read issues' do - expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to include(:read_issue) - expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to include(:read_issue) - expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to include(:read_issue, :update_issue) - expect(permissions(author, issue)).not_to include(:admin_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to include(:read_issue) - expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, issue)).not_to include(:admin_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) - expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end context 'with confidential issues' do @@ -73,37 +73,37 @@ describe IssuePolicy, models: true do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow non-members to read confidential issues' do - expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end end end @@ -123,37 +123,37 @@ describe IssuePolicy, models: true do end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to include(:read_issue) - expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to include(:read_issue) - expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to include(:read_issue, :update_issue) - expect(permissions(author, issue)).not_to include(:admin_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to include(:read_issue) - expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, issue)).not_to include(:admin_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) - expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end context 'with confidential issues' do @@ -161,32 +161,32 @@ describe IssuePolicy, models: true do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows reporter from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) - expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) end end end diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb index 58aa1145c9e..4d6350fc653 100644 --- a/spec/policies/personal_snippet_policy_spec.rb +++ b/spec/policies/personal_snippet_policy_spec.rb @@ -14,7 +14,7 @@ describe PersonalSnippetPolicy, models: true do end def permissions(user) - described_class.abilities(user, snippet).to_set + described_class.new(user, snippet) end context 'public snippet' do @@ -24,9 +24,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(nil) } it do - is_expected.to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -34,9 +34,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(regular_user) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -44,9 +44,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(snippet.author) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*author_permissions) end end end @@ -58,9 +58,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(nil) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -68,9 +68,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(regular_user) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -78,9 +78,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(external_user) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -88,9 +88,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(snippet.author) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*author_permissions) end end end @@ -102,9 +102,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(nil) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -112,9 +112,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(regular_user) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -122,9 +122,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(external_user) } it do - is_expected.not_to include(:read_personal_snippet) - is_expected.not_to include(:comment_personal_snippet) - is_expected.not_to include(*author_permissions) + is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*author_permissions) end end @@ -132,9 +132,9 @@ describe PersonalSnippetPolicy, models: true do subject { permissions(snippet.author) } it do - is_expected.to include(:read_personal_snippet) - is_expected.to include(:comment_personal_snippet) - is_expected.to include(*author_permissions) + is_expected.to be_allowed(:read_personal_snippet) + is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*author_permissions) end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index d70e15f006b..ca435dd0218 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -73,37 +73,45 @@ describe ProjectPolicy, models: true do project.team << [reporter, :reporter] end + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end + it 'does not include the read_issue permission when the issue author is not a member of the private project' do project = create(:empty_project, :private) issue = create(:issue, project: project) user = issue.author - expect(project.team.member?(issue.author)).to eq(false) + expect(project.team.member?(issue.author)).to be false - 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 + expect(Ability).not_to be_allowed(user, :read_issue, project) end - it 'does not include the wiki permissions when the feature is disabled' do - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) - wiki_permissions = [:read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code] + context 'when the feature is disabled' do + subject { described_class.new(owner, project) } - permissions = described_class.abilities(owner, project).to_set + before do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + end - expect(permissions).not_to include(*wiki_permissions) + it 'does not include the wiki permissions' do + expect_disallowed :read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code + end end context 'abilities for non-public projects' do let(:project) { create(:empty_project, namespace: owner.namespace) } - subject { described_class.abilities(current_user, project).to_set } + subject { described_class.new(current_user, project) } context 'with no user' do let(:current_user) { nil } - it { is_expected.to be_empty } + it { is_expected.to be_banned } end context 'guests' do @@ -114,18 +122,18 @@ describe ProjectPolicy, models: true do end it do - is_expected.to include(*guest_permissions) - is_expected.not_to include(*reporter_public_build_permissions) - is_expected.not_to include(*team_member_reporter_permissions) - is_expected.not_to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_disallowed(*reporter_public_build_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end context 'public builds enabled' do it do - is_expected.to include(*guest_permissions) - is_expected.to include(:read_build, :read_pipeline) + expect_allowed(*guest_permissions) + expect_allowed(:read_build, :read_pipeline) end end @@ -135,8 +143,8 @@ describe ProjectPolicy, models: true do end it do - is_expected.to include(*guest_permissions) - is_expected.not_to include(:read_build, :read_pipeline) + expect_allowed(*guest_permissions) + expect_disallowed(:read_build, :read_pipeline) end end @@ -147,8 +155,8 @@ describe ProjectPolicy, models: true do end it do - is_expected.not_to include(:read_build) - is_expected.to include(:read_pipeline) + expect_disallowed(:read_build) + expect_allowed(:read_pipeline) end end end @@ -157,12 +165,13 @@ describe ProjectPolicy, models: true do let(:current_user) { reporter } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.not_to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -170,12 +179,12 @@ describe ProjectPolicy, models: true do let(:current_user) { dev } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.not_to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_disallowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -183,12 +192,12 @@ describe ProjectPolicy, models: true do let(:current_user) { master } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.to include(*master_permissions) - is_expected.not_to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*master_permissions) + expect_disallowed(*owner_permissions) end end @@ -196,12 +205,12 @@ describe ProjectPolicy, models: true do let(:current_user) { owner } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end @@ -209,12 +218,12 @@ describe ProjectPolicy, models: true do let(:current_user) { admin } it do - is_expected.to include(*guest_permissions) - is_expected.to include(*reporter_permissions) - is_expected.not_to include(*team_member_reporter_permissions) - is_expected.to include(*developer_permissions) - is_expected.to include(*master_permissions) - is_expected.to include(*owner_permissions) + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*master_permissions) + expect_allowed(*owner_permissions) end end end diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index d2b2528c57a..2799f03fb9b 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -15,7 +15,15 @@ describe ProjectSnippetPolicy, models: true do def abilities(user, snippet_visibility) snippet = create(:project_snippet, snippet_visibility, project: project) - described_class.abilities(user, snippet).to_set + described_class.new(user, snippet) + end + + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } end context 'public snippet' do @@ -23,8 +31,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(nil, :public) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -32,8 +40,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(regular_user, :public) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -41,8 +49,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(external_user, :public) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end end @@ -52,8 +60,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(nil, :internal) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -61,8 +69,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(regular_user, :internal) } it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -70,8 +78,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(external_user, :internal) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -83,8 +91,8 @@ describe ProjectSnippetPolicy, models: true do end it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end end @@ -94,8 +102,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(nil, :private) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -103,19 +111,19 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(regular_user, :private) } it do - is_expected.not_to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_disallowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end context 'snippet author' do let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } - subject { described_class.abilities(regular_user, snippet).to_set } + subject { described_class.new(regular_user, snippet) } it do - is_expected.to include(:read_project_snippet) - is_expected.to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_allowed(*author_permissions) end end @@ -127,8 +135,8 @@ describe ProjectSnippetPolicy, models: true do end it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -140,8 +148,8 @@ describe ProjectSnippetPolicy, models: true do end it do - is_expected.to include(:read_project_snippet) - is_expected.not_to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_disallowed(*author_permissions) end end @@ -149,8 +157,8 @@ describe ProjectSnippetPolicy, models: true do subject { abilities(create(:admin), :private) } it do - is_expected.to include(:read_project_snippet) - is_expected.to include(*author_permissions) + expect_allowed(:read_project_snippet) + expect_allowed(*author_permissions) end end end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index d5761390d39..0251d5dcf1c 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -4,34 +4,34 @@ describe UserPolicy, models: true do let(:current_user) { create(:user) } let(:user) { create(:user) } - subject { described_class.abilities(current_user, user).to_set } + subject { UserPolicy.new(current_user, user) } describe "reading a user's information" do - it { is_expected.to include(:read_user) } + it { is_expected.to be_allowed(:read_user) } end describe "destroying a user" do context "when a regular user tries to destroy another regular user" do - it { is_expected.not_to include(:destroy_user) } + it { is_expected.not_to be_allowed(:destroy_user) } end context "when a regular user tries to destroy themselves" do let(:current_user) { user } - it { is_expected.to include(:destroy_user) } + it { is_expected.to be_allowed(:destroy_user) } end context "when an admin user tries to destroy a regular user" do let(:current_user) { create(:user, :admin) } - it { is_expected.to include(:destroy_user) } + it { is_expected.to be_allowed(:destroy_user) } end context "when an admin user tries to destroy a ghost user" do let(:current_user) { create(:user, :admin) } let(:user) { create(:user, :ghost) } - it { is_expected.not_to include(:destroy_user) } + it { is_expected.not_to be_allowed(:destroy_user) } end end end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index b8ca73c321c..cdb60fc0d1a 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -164,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 @@ -190,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' @@ -199,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 } @@ -210,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') diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index f169e6661d1..1d8aaeea8f2 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -4,6 +4,13 @@ describe API::Features do let(:user) { create(:user) } let(:admin) { create(:admin) } + before do + Flipper.unregister_groups + Flipper.register(:perf_team) do |actor| + actor.respond_to?(:admin) && actor.admin? + end + end + describe 'GET /features' do let(:expected_features) do [ @@ -16,6 +23,14 @@ describe API::Features do 'name' => 'feature_2', 'state' => 'off', 'gates' => [{ 'key' => 'boolean', 'value' => false }] + }, + { + 'name' => 'feature_3', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'groups', 'value' => ['perf_team'] } + ] } ] end @@ -23,6 +38,7 @@ describe API::Features do before do Feature.get('feature_1').enable Feature.get('feature_2').disable + Feature.get('feature_3').enable Feature.group(:perf_team) end it 'returns a 401 for anonymous users' do @@ -47,30 +63,70 @@ describe API::Features do describe 'POST /feature' do let(:feature_name) { 'my_feature' } - it 'returns a 401 for anonymous users' do - post api("/features/#{feature_name}") - expect(response).to have_http_status(401) - end + context 'when the feature does not exist' do + it 'returns a 401 for anonymous users' do + post api("/features/#{feature_name}") - it 'returns a 403 for users' do - post api("/features/#{feature_name}", user) + expect(response).to have_http_status(401) + end - expect(response).to have_http_status(403) - end + it 'returns a 403 for users' do + post api("/features/#{feature_name}", user) - it 'creates an enabled feature if passed true' do - post api("/features/#{feature_name}", admin), value: 'true' + expect(response).to have_http_status(403) + end - expect(response).to have_http_status(201) - expect(Feature.get(feature_name)).to be_enabled - end + context 'when passed value=true' do + it 'creates an enabled feature' do + post api("/features/#{feature_name}", admin), value: 'true' - it 'creates a feature with the given percentage if passed an integer' do - post api("/features/#{feature_name}", admin), value: '50' + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'on', + 'gates' => [{ 'key' => 'boolean', 'value' => true }]) + end + + it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do + post api("/features/#{feature_name}", admin), value: 'true', feature_group: 'perf_team' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'groups', 'value' => ['perf_team'] } + ]) + end + + it 'creates an enabled feature for the given user when passed user=username' do + post api("/features/#{feature_name}", admin), value: 'true', user: user.username - expect(response).to have_http_status(201) - expect(Feature.get(feature_name).percentage_of_time_value).to be(50) + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["User:#{user.id}"] } + ]) + end + end + + it 'creates a feature with the given percentage if passed an integer' do + post api("/features/#{feature_name}", admin), value: '50' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'percentage_of_time', 'value' => 50 } + ]) + end end context 'when the feature exists' do @@ -80,11 +136,83 @@ describe API::Features do feature.disable # This also persists the feature on the DB end - it 'enables the feature if passed true' do - post api("/features/#{feature_name}", admin), value: 'true' + context 'when passed value=true' do + it 'enables the feature' do + post api("/features/#{feature_name}", admin), value: 'true' - expect(response).to have_http_status(201) - expect(feature).to be_enabled + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'on', + 'gates' => [{ 'key' => 'boolean', 'value' => true }]) + end + + it 'enables the feature for the given Flipper group when passed feature_group=perf_team' do + post api("/features/#{feature_name}", admin), value: 'true', feature_group: 'perf_team' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'groups', 'value' => ['perf_team'] } + ]) + end + + it 'enables the feature for the given user when passed user=username' do + post api("/features/#{feature_name}", admin), value: 'true', user: user.username + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["User:#{user.id}"] } + ]) + end + end + + context 'when feature is enabled and value=false is passed' do + it 'disables the feature' do + feature.enable + expect(feature).to be_enabled + + post api("/features/#{feature_name}", admin), value: 'false' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'off', + 'gates' => [{ 'key' => 'boolean', 'value' => false }]) + end + + it 'disables the feature for the given Flipper group when passed feature_group=perf_team' do + feature.enable(Feature.group(:perf_team)) + expect(Feature.get(feature_name).enabled?(admin)).to be_truthy + + post api("/features/#{feature_name}", admin), value: 'false', feature_group: 'perf_team' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'off', + 'gates' => [{ 'key' => 'boolean', 'value' => false }]) + end + + it 'disables the feature for the given user when passed user=username' do + feature.enable(user) + expect(Feature.get(feature_name).enabled?(user)).to be_truthy + + post api("/features/#{feature_name}", admin), value: 'false', user: user.username + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'off', + 'gates' => [{ 'key' => 'boolean', 'value' => false }]) + end end context 'with a pre-existing percentage value' do @@ -96,7 +224,13 @@ describe API::Features do post api("/features/#{feature_name}", admin), value: '30' expect(response).to have_http_status(201) - expect(Feature.get(feature_name).percentage_of_time_value).to be(30) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'percentage_of_time', 'value' => 30 } + ]) end end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 3bf16a3ae27..26cf653ca8e 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -15,6 +15,20 @@ describe API::Namespaces do end context "when authenticated as admin" do + it "returns correct attributes" do + get api("/namespaces", admin) + + group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' } + user_kind_json_response = json_response.find { |resource| resource['kind'] == 'user' } + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(group_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'members_count_with_descendants') + + expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id') + end + it "admin: returns an array of all namespaces" do get api("/namespaces", admin) @@ -37,6 +51,27 @@ describe API::Namespaces do end context "when authenticated as a regular user" do + it "returns correct attributes when user can admin group" do + group1.add_owner(user) + + get api("/namespaces", user) + + owned_group_response = json_response.find { |resource| resource['id'] == group1.id } + + expect(owned_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'members_count_with_descendants') + end + + it "returns correct attributes when user cannot admin group" do + group1.add_guest(user) + + get api("/namespaces", user) + + guest_group_response = json_response.find { |resource| resource['id'] == group1.id } + + expect(guest_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id') + end + it "user: returns an array of namespaces" do get api("/namespaces", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index fd7ff0b9cff..14dec3d45b1 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -698,7 +698,8 @@ describe API::Projects do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, - 'full_path' => user.namespace.full_path + 'full_path' => user.namespace.full_path, + 'parent_id' => nil }) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index bf7ed2d3ad6..aa95ae8c7cc 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -395,6 +395,7 @@ describe API::Users do 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') @@ -427,13 +428,22 @@ describe API::Users do 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) @@ -441,12 +451,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') @@ -454,12 +466,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 @@ -467,6 +481,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) @@ -474,6 +489,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 @@ -490,6 +506,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 @@ -540,6 +557,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 @@ -547,6 +565,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 diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index cb74868324c..af44ffa2331 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -734,7 +734,8 @@ describe API::V3::Projects do 'name' => user.namespace.name, 'path' => user.namespace.path, 'kind' => user.namespace.kind, - 'full_path' => user.namespace.full_path + 'full_path' => user.namespace.full_path, + 'parent_id' => nil }) end 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/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 95d40138fea..2f1c3c95e59 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -246,28 +246,13 @@ describe 'project routing' do end end - # diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs - # commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits - # merge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/merge(.:format) projects/merge_requests#merge - # ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status - # toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription - # branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from - # branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to - # update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches - # namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index - # POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create - # new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new - # edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit - # namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show - # PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update - # PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update describe Projects::MergeRequestsController, 'routing' do - it 'to #diffs' do - expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + it 'to #commits' do + expect(get('/gitlab/gitlabhq/merge_requests/1/commits.json')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end - it 'to #commits' do - expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + it 'to #pipelines' do + expect(get('/gitlab/gitlabhq/merge_requests/1/pipelines.json')).to route_to('projects/merge_requests#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end it 'to #merge' do @@ -277,25 +262,59 @@ describe 'project routing' do ) end + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') + expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs') + expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'commits') + expect(get('/gitlab/gitlabhq/merge_requests/1/pipelines')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'pipelines') + end + + it_behaves_like 'RESTful project resources' do + let(:controller) { 'merge_requests' } + let(:actions) { [:index, :edit, :show, :update] } + end + end + + describe Projects::MergeRequests::CreationsController, 'routing' do + it 'to #new' do + expect(get('/gitlab/gitlabhq/merge_requests/new')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/new/diffs')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'diffs') + expect(get('/gitlab/gitlabhq/merge_requests/new/pipelines')).to route_to('projects/merge_requests/creations#new', namespace_id: 'gitlab', project_id: 'gitlabhq', tab: 'pipelines') + end + + it 'to #create' do + expect(post('/gitlab/gitlabhq/merge_requests')).to route_to('projects/merge_requests/creations#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + it 'to #branch_from' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/new/branch_from')).to route_to('projects/merge_requests/creations#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #branch_to' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/new/branch_to')).to route_to('projects/merge_requests/creations#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it 'to #show' do - expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') - expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') + it 'to #pipelines' do + expect(get('/gitlab/gitlabhq/merge_requests/new/pipelines.json')).to route_to('projects/merge_requests/creations#pipelines', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') end - it_behaves_like 'RESTful project resources' do - let(:controller) { 'merge_requests' } - let(:actions) { [:index, :create, :new, :edit, :show, :update] } + it 'to #diffs' do + expect(get('/gitlab/gitlabhq/merge_requests/new/diffs.json')).to route_to('projects/merge_requests/creations#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'json') + end + end + + describe Projects::MergeRequests::DiffsController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs.json')).to route_to('projects/merge_requests/diffs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'json') end end + describe Projects::MergeRequests::ConflictsController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/merge_requests/1/conflicts')).to route_to('projects/merge_requests/conflicts#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + end # raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw # project_snippets GET /:project_id/snippets(.:format) snippets#index # POST /:project_id/snippets(.:format) snippets#create 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/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/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index ac7ccfbaab0..213678c27f5 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -12,7 +12,6 @@ describe GitHooksService, services: true do @oldrev = sample_commit.parent_id @newrev = sample_commit.id @ref = 'refs/heads/feature' - @repo_path = project.repository.path_to_repo end describe '#execute' do @@ -21,7 +20,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } + service.execute(user, project, @blankrev, @newrev, @ref) { } end end @@ -31,7 +30,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end @@ -43,7 +42,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index a37257d1bf4..d59b37bee36 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -15,6 +15,14 @@ describe Groups::DestroyService, services: true do group.add_user(user, Gitlab::Access::OWNER) end + def destroy_group(group, user, async) + if async + Groups::DestroyService.new(group, user).async_execute + else + Groups::DestroyService.new(group, user).execute + end + end + shared_examples 'group destruction' do |async| context 'database records' do before do @@ -30,30 +38,14 @@ describe Groups::DestroyService, services: true do context 'file system' do context 'Sidekiq inline' do before do - # Run sidekiq immediatly to check that renamed dir will be removed + # Run sidekiq immediately to check that renamed dir will be removed Sidekiq::Testing.inline! { destroy_group(group, user, async) } end - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey } - end - - context 'Sidekiq fake' do - before do - # Don't run sidekiq to check if renamed repository exists - Sidekiq::Testing.fake! { destroy_group(group, user, async) } + it 'verifies that paths have been deleted' do + expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey + expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey end - - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy } - end - end - - def destroy_group(group, user, async) - if async - Groups::DestroyService.new(group, user).async_execute - else - Groups::DestroyService.new(group, user).execute end end end @@ -61,6 +53,26 @@ describe Groups::DestroyService, services: true do describe 'asynchronous delete' do it_behaves_like 'group destruction', true + context 'Sidekiq fake' do + before do + # Don't run Sidekiq to verify that group and projects are not actually destroyed + Sidekiq::Testing.fake! { destroy_group(group, user, true) } + end + + after do + # Clean up stale directories + gitlab_shell.rm_namespace(project.repository_storage_path, group.path) + gitlab_shell.rm_namespace(project.repository_storage_path, remove_path) + end + + it 'verifies original paths and projects still exist' do + expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_truthy + expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey + expect(Project.unscoped.count).to eq(1) + expect(Group.unscoped.count).to eq(2) + end + end + context 'potential race conditions' do context "when the `GroupDestroyWorker` task runs immediately" do it "deletes the group" do diff --git a/spec/services/notification_recipient_service_spec.rb b/spec/services/notification_recipient_service_spec.rb new file mode 100644 index 00000000000..dfe1ee7c41e --- /dev/null +++ b/spec/services/notification_recipient_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe NotificationRecipientService, services: true do + set(:user) { create(:user) } + set(:project) { create(:empty_project, :public) } + set(:issue) { create(:issue, project: project) } + + set(:watcher) do + watcher = create(:user) + setting = watcher.notification_settings_for(project) + setting.level = :watch + setting.save + + watcher + end + + subject { described_class.new(project) } + + describe '#build_recipients' do + it 'does not modify the participants of the target' do + expect { subject.build_recipients(issue, user, action: :new_issue) } + .not_to change { issue.participants(user) } + end + end + + describe '#build_new_note_recipients' do + set(:note) { create(:note_on_issue, noteable: issue, project: project) } + + it 'does not modify the participants of the target' do + expect { subject.build_new_note_recipients(note) } + .not_to change { note.noteable.participants(note.author) } + 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/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/support/fake_migration_classes.rb b/spec/support/fake_migration_classes.rb index 3de0460c3ca..b0fc8422857 100644 --- a/spec/support/fake_migration_classes.rb +++ b/spec/support/fake_migration_classes.rb @@ -1,3 +1,11 @@ class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration include Gitlab::Database::RenameReservedPathsMigration::V1 + + def version + '20170316163845' + end + + def name + "FakeRenameReservedPathMigrationV1" + end end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 50869099bb7..98b014df6cd 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -28,7 +28,12 @@ shared_examples 'issuable record that supports quick actions in its description describe "new #{issuable_type}", js: true do context 'with commands in the description' do it "creates the #{issuable_type} and interpret commands accordingly" do - visit public_send("new_namespace_project_#{issuable_type}_path", project.namespace, project, new_url_opts) + case issuable_type + when :merge_request + visit public_send("namespace_project_new_merge_request_path", project.namespace, project, new_url_opts) + when :issue + visit public_send("new_namespace_project_issue_path", project.namespace, project, new_url_opts) + end fill_in "#{issuable_type}_title", with: 'bug 345' fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\"" click_button "Submit #{issuable_type}".humanize 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/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/login_helpers.rb b/spec/support/login_helpers.rb index 879386b5437..4c88958264b 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -89,4 +89,25 @@ module LoginHelpers }) Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml] end + + 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 + + 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/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb new file mode 100644 index 00000000000..fb43f51c70c --- /dev/null +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -0,0 +1,84 @@ +# AccessMatchersForController +# +# For testing authorize_xxx in controller. +module AccessMatchersForController + extend RSpec::Matchers::DSL + include Warden::Test::Helpers + + EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 302].freeze + EXPECTED_STATUS_CODE_DENIED = [401, 404].freeze + + def emulate_user(role, membership = nil) + case role + when :admin + user = create(:admin) + sign_in(user) + when :user + user = create(:user) + sign_in(user) + when :external + user = create(:user, external: true) + sign_in(user) + when :visitor + user = nil + when User + user = role + sign_in(user) + when *Gitlab::Access.sym_options_with_owner.keys # owner, master, developer, reporter, guest + raise ArgumentError, "cannot emulate #{role} without membership parent" unless membership + + user = create_user_by_membership(role, membership) + sign_in(user) + else + raise ArgumentError, "cannot emulate user #{role}" + end + + user + end + + def create_user_by_membership(role, membership) + if role == :owner && membership.owner + user = membership.owner + else + user = create(:user) + membership.public_send(:"add_#{role}", user) + end + user + end + + def description_for(role, type, expected, result) + "be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}" + end + + matcher :be_allowed_for do |role| + match do |action| + emulate_user(role, @membership) + action.call + + EXPECTED_STATUS_CODE_ALLOWED.include?(response.status) + end + + chain :of do |membership| + @membership = membership + end + + description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) } + supports_block_expectations + end + + matcher :be_denied_for do |role| + match do |action| + emulate_user(role, @membership) + action.call + + EXPECTED_STATUS_CODE_DENIED.include?(response.status) + end + + chain :of do |membership| + @membership = membership + end + + description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) } + supports_block_expectations + end +end 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/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/shared_examples/features/issuable_sidebar_shared_examples.rb b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb new file mode 100644 index 00000000000..96c821b26f7 --- /dev/null +++ b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb @@ -0,0 +1,9 @@ +shared_examples 'issue sidebar stays collapsed on mobile' do + before do + resize_screen_xs + end + + it 'keeps the sidebar collapsed' do + expect(page).not_to have_css('.right-sidebar.right-sidebar-collapsed') + end +end 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/stub_env.rb b/spec/support/stub_env.rb index 18597b5c71f..b8928867174 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/stub_env.rb @@ -1,7 +1,33 @@ +# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb module StubENV - def stub_env(key, value) - allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed - @env_already_stubbed ||= true + def stub_env(key_or_hash, value = nil) + init_stub unless env_stubbed? + if key_or_hash.is_a? Hash + key_or_hash.each { |k, v| add_stubbed_value(k, v) } + else + add_stubbed_value key_or_hash, value + end + end + + private + + STUBBED_KEY = '__STUBBED__'.freeze + + def add_stubbed_value(key, value) allow(ENV).to receive(:[]).with(key).and_return(value) + allow(ENV).to receive(:fetch).with(key).and_return(value) + allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val| + value || default_val + end + end + + def env_stubbed? + ENV[STUBBED_KEY] + end + + def init_stub + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:fetch).and_call_original + add_stubbed_value(STUBBED_KEY, true) end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1c5267c290b..32546abcad4 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -120,18 +120,21 @@ module TestEnv end def setup_gitlab_shell - unless File.directory?(Gitlab.config.gitlab_shell.path) - unless system('rake', 'gitlab:shell:install') - raise 'Can`t clone gitlab-shell' - end + shell_needs_update = component_needs_update?(Gitlab.config.gitlab_shell.path, + Gitlab::Shell.version_required) + + unless !shell_needs_update || system('rake', 'gitlab:shell:install') + raise 'Can`t clone gitlab-shell' end end def setup_gitaly socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) + gitaly_needs_update = component_needs_update?(gitaly_dir, + Gitlab::GitalyClient.expected_server_version) - unless !gitaly_needs_update?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") + unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") raise "Can't clone gitaly" end @@ -261,13 +264,13 @@ module TestEnv end end - def gitaly_needs_update?(gitaly_dir) - gitaly_version = File.read(File.join(gitaly_dir, 'VERSION')).strip + def component_needs_update?(component_folder, expected_version) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions # (`=branch_name`), but that actually makes sure the server is always based # on the latest branch revision. - gitaly_version != Gitlab::GitalyClient.expected_server_version + version != expected_version rescue Errno::ENOENT true end 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/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index 4052dbf8df3..3e17fe2104b 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/merge_requests/show/_commits.html.haml' do +describe 'projects/merge_requests/_commits.html.haml' do include Devise::Test::ControllerHelpers let(:user) { create(:user) } diff --git a/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb index 4f698a34ab5..1e9bdf9108f 100644 --- a/spec/views/projects/merge_requests/_new_submit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/merge_requests/_new_submit.html.haml', :view do +describe 'projects/merge_requests/creations/_new_submit.html.haml', :view do let(:merge_request) { create(:merge_request) } let!(:pipeline) { create(:ci_empty_pipeline) } diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index cc9bc29c6cc..a8f4bb72acf 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -4,7 +4,7 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } - let(:project_identifier) { "project-#{project.id}" } + let(:gl_repository) { "project-#{project.id}" } let(:key) { create(:key, user: project.owner) } let(:key_id) { key.shell_id } @@ -19,22 +19,14 @@ describe PostReceive do end context 'with a non-existing project' do - let(:project_identifier) { "project-123456789" } + let(:gl_repository) { "project-123456789" } let(:error_message) do - "Triggered hook for non-existing project with identifier \"#{project_identifier}\"" + "Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"" end it "returns false and logs an error" do expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}") - expect(described_class.new.perform(project_identifier, key_id, base64_changes)).to be(false) - end - end - - 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, follow_redirects: true).and_call_original - - described_class.new.perform(pwd(project), key_id, base64_changes) + expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be(false) end end @@ -49,7 +41,7 @@ describe PostReceive do it "calls GitTagPushService" do expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -59,7 +51,7 @@ describe PostReceive do it "calls GitTagPushService" do expect_any_instance_of(GitPushService).not_to receive(:execute) expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -69,12 +61,12 @@ describe PostReceive do it "does not call any of the services" do expect_any_instance_of(GitPushService).not_to receive(:execute) expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end context "gitlab-ci.yml" do - subject { described_class.new.perform(project_identifier, key_id, base64_changes) } + subject { described_class.new.perform(gl_repository, key_id, base64_changes) } context "creates a Ci::Pipeline for every change" do before do @@ -111,7 +103,7 @@ describe PostReceive do it 'calls SystemHooksService' do expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end end @@ -119,7 +111,7 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_by).with(id: project.id.to_s) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end it "does not run if the author is not in the project" do @@ -129,7 +121,7 @@ describe PostReceive do expect(project).not_to receive(:execute_hooks) - expect(described_class.new.perform(project_identifier, key_id, base64_changes)).to be_falsey + expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be_falsey end it "asks the project to trigger all hooks" do @@ -137,18 +129,14 @@ describe PostReceive do expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end it "enqueues a UpdateMergeRequestsWorker job" do allow(Project).to receive(:find_by).and_return(project) expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) - described_class.new.perform(project_identifier, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) end end - - def pwd(project) - File.join(Gitlab.config.repositories.storages.default['path'], project.path_with_namespace) - end end 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 |