diff options
1089 files changed, 12439 insertions, 7893 deletions
diff --git a/.eslintignore b/.eslintignore index 1605e483e9e..1623b996213 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,4 +7,5 @@ /vendor/ karma.config.js webpack.config.js -/app/assets/javascripts/locale/**/*.js +svg.config.js +/app/assets/javascripts/locale/**/app.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 766fb3a2ef7..ed993abae73 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -128,7 +128,7 @@ stages: - export CACHE_CLASSES=true - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - scripts/gitaly-test-spawn - - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' + - knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d when: always @@ -174,12 +174,22 @@ build-package: # Review docs base .review-docs: &review-docs image: ruby:2.4-alpine - before_script: [] + before_script: + - gem install gitlab --no-doc + # We need to download the script rather than clone the repo since the + # review-docs-cleanup job will not be able to run when the branch gets + # deleted (when merging the MR). + - apk add --update openssl + - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs + - chmod 755 trigger-build-docs services: [] + cache: {} + dependencies: [] + artifacts: {} variables: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" - cache: {} + GIT_STRATEGY: none when: manual only: - branches @@ -193,11 +203,10 @@ review-docs-deploy: name: review-docs/$CI_COMMIT_REF_NAME # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 - url: http://$CI_COMMIT_REF_SLUG-built-from-ce-ee.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX + url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup script: - - gem install gitlab --no-doc - - scripts/trigger-build-docs deploy + - ./trigger-build-docs deploy # Cleanup remote environment of gitlab-docs review-docs-cleanup: @@ -207,8 +216,7 @@ review-docs-cleanup: name: review-docs/$CI_COMMIT_REF_NAME action: stop script: - - gem install gitlab --no-doc - - scripts/trigger-build-docs cleanup + - ./trigger-build-docs cleanup # Retrieve knapsack and rspec_flaky reports retrieve-tests-metadata: @@ -413,12 +421,12 @@ downtime_check: ee_compat_check: <<: *rake-exec - only: - - branches@gitlab-org/gitlab-ce except: - master - tags - /^[\d-]+-stable(-ee)?/ + - branches@gitlab-org/gitlab-ee + - branches@gitlab/gitlab-ee allow_failure: yes cache: key: "ee_compat_check_repo" @@ -517,6 +525,12 @@ db:seed_fu-mysql: <<: *db-seed_fu <<: *use-mysql +db:check-schema-pg: + <<: *db-migrate-reset + <<: *use-pg + script: + - source scripts/schema_changed.sh + # Frontend-related jobs gitlab:assets:compile: <<: *dedicated-runner diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cedfa60b3e..3321ace28fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,218 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.0.2 (2017-09-27) + +- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450 +- [FIXED] Some checks in `rake gitlab:check` were failling with 'undefined method `run_command`'. !14469 +- [FIXED] Make locked setting of Runner to not affect jobs scheduling. !14483 +- [FIXED] Re-allow `name` attribute on user-provided anchor HTML. + +## 10.0.1 (2017-09-23) + +- [FIXED] Fix duplicate key errors in PostDeployMigrateUserExternalMailData migration. + +## 10.0.0 (2017-09-22) + +- [SECURITY] Upgrade brace-expansion NPM package due to security issue. !13665 (Markus Koller) +- [REMOVED] Remove CI API v1. +- [FIXED] Ensure correct visibility level options shown on all Project, Group, and Snippets forms. !13442 +- [FIXED] Fix the /projects/:id/repository/files/:file_path/raw endpoint to handle dots in the file_path. !13512 (mahcsig) +- [FIXED] Merge request reference in merge commit changed to full reference. !13518 (haseebeqx) +- [FIXED] Removes Sortable default scope. !13558 +- [FIXED] Wiki table of contents are now properly nested to reflect header level. !13650 (Akihiro Nakashima) +- [FIXED] Improve bare project import: Allow subgroups, take default visibility level into account. !13670 +- [FIXED] Fix group and project search for anonymous users. !13745 +- [FIXED] Fix searching for files by path. !13798 +- [FIXED] Fix division by zero error in blame age mapping. !13803 (Jeff Stubler) +- [FIXED] Fix incorrect date/time formatting on prometheus graphs. !13865 +- [FIXED] Changes the password change workflow for admins. !13901 +- [FIXED] API: Respect default group visibility when creating a group. !13903 (Robert Schilling) +- [FIXED] Unescape HTML characters in Wiki title. !13942 (Jacopo Beschi @jacopo-beschi) +- [FIXED] Make blob viewer for rich contents wider for mobile. !14011 (Takuya Noguchi) +- [FIXED] Fix typo in the API Deploy Keys documentation page. !14014 (Vitaliy @blackst0ne Klachkov) +- [FIXED] Hide admin link from default search results for non-admins. !14015 +- [FIXED] Fix problems sanitizing URLs with empty passwords. !14083 +- [FIXED] Fix stray OR in New Project page. !14096 (Robin Bobbitt) +- [FIXED] Fix a wrong `X-Gitlab-Event` header when testing webhooks. !14108 +- [FIXED] Fix the diff file header from being html escaped for renamed files. !14121 +- [FIXED] Image attachments are properly displayed in notification emails again. !14161 +- [FIXED] Fixes the 500 errors caused by a race condition in GPG's tmp directory handling. !14194 (Alexis Reigel) +- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242 +- [FIXED] Fix Pipeline Triggers to show triggered label and predefined variables (e.g. CI_PIPELINE_TRIGGERED). !14244 +- [FIXED] Allow using newlines in pipeline email service recipients. !14250 +- [FIXED] Fix errors when moving issue with reference to a group milestone. !14294 +- [FIXED] Fix the "resolve discussion in a new issue" button. !14357 +- [FIXED] File uploaders do not perform hard check, only soft check. +- [FIXED] Add to_project_id parameter to Move Issue via API example. +- [FIXED] Update x/x discussions resolved checkmark icon to be green when all discussions resolved. +- [FIXED] Fixed add diff note button not showing after deleting a comment. +- [FIXED] Fix broken svg in jobs dropdown for success status. +- [FIXED] Fix buttons with different height in merge request widget. +- [FIXED] Removes disabled state from dashboard project button. +- [FIXED] Better align fallback image emojis. +- [FIXED] Remove focus styles from dropdown empty links. +- [FIXED] Fix inconsistent spacing for edit buttons on issues and merge request page. +- [FIXED] Fix edit merge request and issues button inconsistent letter casing. +- [FIXED] Improve Import/Export memory usage. +- [FIXED] Fix Import/Export issue to do with fork merge requests. +- [FIXED] Fix invite by email address duplication. +- [FIXED] Adds tooltip to the branch name and improves performance. +- [FIXED] Disable GitLab Project Import Button if source disabled. +- [FIXED] Migrate issues authored by deleted user to the Ghost user. +- [FIXED] Fix new navigation wrapping and causing height to grow. +- [FIXED] Normalize styles for empty state combo button. +- [FIXED] Fix external link to Composer website. +- [FIXED] Prevents jobs dropdown from closing in pipeline graph. +- [FIXED] Include the `is_admin` field in the `GET /users/:id` API when current user is an admin. +- [FIXED] Fix breadcrumbs container in issue boards. +- [FIXED] Fix project feature being deleted when updating project with invalid visibility level. +- [FIXED] Truncate milestone title if sidebar is collapsed. +- [FIXED] Prevents rendering empty badges when request fails. +- [FIXED] Fixes margins on the top buttons of the pipeline table. +- [FIXED] Bump jira-ruby gem to 1.4.1 to fix issues with HTTP proxies. +- [FIXED] Eliminate N+1 queries in loading discussions.json endpoint. +- [FIXED] Eliminate N+1 queries referencing issues. +- [FIXED] Remove unnecessary loading of discussions in `IssuesController#show`. +- [FIXED] Fix errors thrown in merge request widget with external CI service/integration. +- [FIXED] Do not show the Auto DevOps banner when the project has a .gitlab-ci.yml on master. +- [FIXED] Reword job to pipeline to reflect what the graphs are really about. +- [FIXED] Sort templates in the dropdown. +- [FIXED] Fix Auto DevOps banner to be shown on empty projects. +- [FIXED] Resolve Image onion skin + swipe does not work anymore. +- [FIXED] Fix mini graph pipeline breakin in merge request view. +- [FIXED] Fixed merge request changes bar jumping. +- [FIXED] Improve migrations using triggers. +- [FIXED] Fix ConvDev Index nav item and Monitoring submenu regression. +- [FIXED] disabling notifications globally now properly turns off group/project added + emails !13325 +- [DEPRECATED] Deprecate custom SSH client configuration for the git user. !13930 +- [CHANGED] allow all users to delete their account. !13636 (Jacopo Beschi @jacopo-beschi) +- [CHANGED] Use full path of project's avatar in webhooks. !13649 (Vitaliy @blackst0ne Klachkov) +- [CHANGED] Add filtered search to group merge requests dashboard. !13688 (Hiroyuki Sato) +- [CHANGED] Fire hooks asynchronously when creating a new job to improve performance. !13734 +- [CHANGED] Improve performance for AutocompleteController#users.json. !13754 (Hiroyuki Sato) +- [CHANGED] Update the GPG verification semantics: A GPG signature must additionally match the committer in order to be verified. !13771 (Alexis Reigel) +- [CHANGED] Support a multi-word fuzzy seach issues/merge requests on search bar. !13780 (Hiroyuki Sato) +- [CHANGED] Default LDAP config "verify_certificates" to true for security. !13915 +- [CHANGED] "Share with group lock" now applies to subgroups, but owner can override setting on subgroups. !13944 +- [CHANGED] Make Gitaly PostUploadPack mandatory. !13953 +- [CHANGED] Remove project select dropdown from breadcrumb. !14010 +- [CHANGED] Redesign project feature permissions settings. !14062 +- [CHANGED] Document version Group Milestones API introduced. +- [CHANGED] Finish migration to the new events setup. +- [CHANGED] restyling of OAuth authorization confirmation. (Jacopo Beschi @jacopo-beschi) +- [CHANGED] Added support for specific labels and colors. +- [CHANGED] Move "Move issue" controls to right-sidebar. +- [CHANGED] Remove pages settings when not available. +- [CHANGED] Allow all AutoDevOps banners to be turned off. +- [CHANGED] Update Rails project template to use Postgresql by default. +- [CHANGED] Added support the multiple time series for prometheus monitoring. +- [ADDED] API: Respect the "If-Unmodified-Since" header when delting a resource. !9621 (Robert Schilling) +- [ADDED] Protected runners. !13194 +- [ADDED] Add support for copying permalink to notes via more actions dropdown. !13299 +- [ADDED] Add API support for wiki pages. !13372 (Vitaliy @blackst0ne Klachkov) +- [ADDED] Add a `Last 7 days` option for Cycle Analytics view. !13443 (Mehdi Lahmam (@mehlah)) +- [ADDED] inherits milestone and labels when a merge request is created from issue. !13461 (haseebeqx) +- [ADDED] Add 'from commit' information to cherry-picked commits. !13475 (Saverio Miroddi) +- [ADDED] Add an option to list only archived projects. !13492 (Mehdi Lahmam (@mehlah)) +- [ADDED] Extend API: Pipeline Schedule Variable. !13653 +- [ADDED] Add settings for minimum SSH key strength and allowed key type. !13712 (Cory Hinshaw) +- [ADDED] Add div id to the readme in the project overview. !13735 (Riccardo Padovani @rpadovani) +- [ADDED] Add CI/CD job predefined variables with user name and login. !13824 +- [ADDED] API: Add GPG key management. !13828 (Robert Schilling) +- [ADDED] Add CI/CD active kubernetes job policy. !13849 +- [ADDED] Add dropdown to Projects nav item. !13866 +- [ADDED] Allow users and administrator to configure Auto-DevOps. !13923 +- [ADDED] Implement `failure_reason` on `ci_builds`. !13937 +- [ADDED] Add branch existence check to the APIv4 branches via HEAD request. !13979 (Vitaliy @blackst0ne Klachkov) +- [ADDED] Add quick submission on user settings page. !14007 (Vitaliy @blackst0ne Klachkov) +- [ADDED] Add my_reaction_emoji param to /issues and /merge_requests API. !14016 (Hiroyuki Sato) +- [ADDED] Make it possible to download a single job artifact file using the API. !14027 +- [ADDED] Add repository toggle for automatically resolving outdated diff discussions. !14053 (AshleyDumaine) +- [ADDED] Scripts to detect orphaned repositories. !14204 +- [ADDED] Created callout for auto devops. +- [ADDED] Add option in preferences to change navigation theme color. +- [ADDED] Add JSON logger in `log/api_json.log` for Grape API endpoints. +- [ADDED] Add CI_PIPELINE_SOURCE variable on CI Jobs. +- [ADDED] Changed message and title on the 404 page. (Branka Martinovic) +- [ADDED] Handle if Auto DevOps domain is not set in project settings. +- [ADDED] Add collapsable sections for Pipeline Settings. +- [OTHER] Add badge for dependency status. !13588 (Markus Koller) +- [OTHER] Migration to remove pending delete projects with non-existing namespace. !13598 +- [OTHER] Bump rouge to v2.2.0. !13633 +- [OTHER] Fix repository equality check and avoid fetching ref if the commit is already available. This affects merge request creation performance. !13685 +- [OTHER] Replace 'source/search_code.feature' spinach test with an rspec analog. !13697 (blackst0ne) +- [OTHER] Remove unwanted refs after importing a project. !13766 +- [OTHER] Never wait for sidekiq jobs when creating projects. !13775 +- [OTHER] Gitaly feature toggles are on by default in development. !13802 +- [OTHER] Remove `is_` prefix from predicate method names. !13810 (Maxim Rydkin) +- [OTHER] Update 'Using Docker images' documentation. !13848 +- [OTHER] Update gpg documentation with gpg2. !13851 (M M Arif) +- [OTHER] Replace 'project/star.feature' spinach test with an rspec analog. !13855 (Vitaliy @blackst0ne Klachkov) +- [OTHER] Replace 'project/user_lookup.feature' spinach test with an rspec analog. !13863 (Vitaliy @blackst0ne Klachkov) +- [OTHER] Bump rouge to v2.2.1. !13887 +- [OTHER] Add documentation for PlantUML in reStructuredText. !13900 (Markus Koller) +- [OTHER] Decrease ABC threshold to 55.25. !13904 (Maxim Rydkin) +- [OTHER] Decrease Cyclomatic Complexity threshold to 14. !13972 (Maxim Rydkin) +- [OTHER] Update documentation for confidential issue. !14117 +- [OTHER] Remove redundant WHERE from event queries. +- [OTHER] Memoize the latest builds of a pipeline on a project's homepage. +- [OTHER] Re-use issue/MR counts for the pagination system. +- [OTHER] Memoize pipelines for project download buttons. +- [OTHER] Reorganize indexes for the "deployments" table. +- [OTHER] Improves markdown rendering performance for commit lists. +- [OTHER] Only update the sidebar count caches when needed. +- [OTHER] Improves performance of vue code by using vue files and moving svg out of data function in pipeline schedule callout. +- [OTHER] Rework how recent push events are retrieved. +- [OTHER] Restyle dropdown menus to make them look consistent. +- [OTHER] Upgrade grape to 1.0. +- [OTHER] Add usage data for Auto DevOps. +- [OTHER] Cache the number of open issues and merge requests. +- [OTHER] Constrain environment deployments to project IDs. +- [OTHER] Eager load namespace owners for project dashboards. +- [OTHER] Add description template examples to documentation. +- [OTHER] Disallow NULL values for environments.project_id. +- Add my reaction filter to search bar. !12962 (Hiroyuki Sato) +- Generalize profile updates from providers. !12968 (Alexandros Keramidas) +- Validate PO-files in static analysis. !13000 +- First-time contributor badge. !13143 (Micaël Bergeron <micaelbergeron@gmail.com>) +- Add option to disable project export on instance. !13211 (Robin Bobbitt) +- Hashed Storage support for Repositories (EXPERIMENTAL). !13246 +- Added tests for commits API unauthenticated user and public/private project. !13287 (Jacopo Beschi @jacopo-beschi) +- Fix CI_PROJECT_PATH_SLUG slugify. !13350 (Ivan Chernov) +- Add checks for branch existence before changing HEAD. !13359 (Vitaliy @blackst0ne Klachkov) +- Fix the alignment of line numbers to lines of code in code viewer. !13403 (Trevor Flynn) +- Allow users to move issues to other projects using a / command. !13436 (Manolis Mavrofidis) +- Bumps omniauth-ldap gem version to 2.0.4. !13465 +- Implement the Gitaly RefService::RefExists endpoint. !13528 (Andrew Newdigate) +- Changed all font-weight values to 400 and 600 and introduced 2 variables to manage them. +- Simplify checking if objects exist code in new issaubles workers. +- Present enqueued merge jobs as Merging as well. +- Don't escape html entities in InlineDiffMarkdownMarker. +- Move ConvDev Index location to after Cohorts. +- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) +- [BUGIFX] Improves subgroup creation permissions. !13418 + +## 9.5.5 (2017-09-18) + +- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) +- [FIXED] Fix division by zero error in blame age mapping. !13803 (Jeff Stubler) +- [FIXED] Fix problems sanitizing URLs with empty passwords. !14083 +- [FIXED] Fix a wrong `X-Gitlab-Event` header when testing webhooks. !14108 +- [FIXED] Fixes the 500 errors caused by a race condition in GPG's tmp directory handling. !14194 (Alexis Reigel) +- [FIXED] Fix Pipeline Triggers to show triggered label and predefined variables (e.g. CI_PIPELINE_TRIGGERED). !14244 +- [FIXED] Fix project feature being deleted when updating project with invalid visibility level. +- [FIXED] Fix new navigation wrapping and causing height to grow. +- [FIXED] Fix buttons with different height in merge request widget. +- [FIXED] Normalize styles for empty state combo button. +- [FIXED] Fix broken svg in jobs dropdown for success status. +- [FIXED] Improve migrations using triggers. +- [FIXED] Disable GitLab Project Import Button if source disabled. +- [CHANGED] Update the GPG verification semantics: A GPG signature must additionally match the committer in order to be verified. !13771 (Alexis Reigel) +- [OTHER] Fix repository equality check and avoid fetching ref if the commit is already available. This affects merge request creation performance. !13685 +- [OTHER] Update documentation for confidential issue. !14117 + ## 9.5.4 (2017-09-06) - [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfb2ce0099a..03521c321a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ _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). +Looking for something to work on? Look for issues with 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 @@ -101,7 +101,7 @@ the remaining issues on the GitHub issue tracker. ## I want to contribute! If you want to contribute to GitLab, but are not sure where to start, -look for [issues with the label `Accepting Merge Requests` and weight < 5][accepting-mrs-weight]. +look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight]. These issues will be of reasonable size and challenge, for anyone to start contributing to GitLab. @@ -209,8 +209,7 @@ We add the ~"Accepting Merge Requests" label to: - Low priority ~bug issues (i.e. we do not add it to the bugs that we want to solve in the ~"Next Patch Release") -- Small ~"feature proposal" that do not need ~UX / ~"Product work", or for which -the ~UX / ~"Product work" is already done +- Small ~"feature proposal" - Small ~"technical debt" issues After adding the ~"Accepting Merge Requests" label, we try to estimate the @@ -223,6 +222,13 @@ know how difficult the issue is. Additionally: - We encourage people that have never contributed to any open source project to look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers] +If you've decided that you would like to work on an issue, please @-mention +the [appropriate product manager](https://about.gitlab.com/handbook/product/#who-to-talk-to-for-what) +as soon as possible. The product manager will then pull in appropriate GitLab team +members to further discuss scope, design, and technical considerations. This will +ensure that that your contribution is aligned with the GitLab product and minimize +any rework and delay in getting it merged into master. + [up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened [firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index ca75280b09b..787ffc30a81 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.38.0 +0.42.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index b3d91f9cfc0..da902181863 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.9.0 +5.9.2 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 4a36342fcab..fd2a01863fd 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -3.0.0 +3.1.0 @@ -26,7 +26,7 @@ gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper-openid_connect', '~> 1.1.0' gem 'omniauth', '~> 1.4.2' gem 'omniauth-auth0', '~> 1.4.1' -gem 'omniauth-azure-oauth2', '~> 0.0.6' +gem 'omniauth-azure-oauth2', '~> 0.0.9' gem 'omniauth-cas3', '~> 1.1.4' gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-github', '~> 1.1.1' @@ -116,7 +116,7 @@ gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' gem 'deckar01-task_list', '2.0.0' -gem 'gitlab-markup', '~> 1.5.1' +gem 'gitlab-markup', '~> 1.6.2' gem 'redcarpet', '~> 3.4' gem 'RedCloth', '~> 4.3.2' gem 'rdoc', '~> 4.2' @@ -128,7 +128,7 @@ gem 'asciidoctor-plantuml', '0.0.7' gem 'rouge', '~> 2.0' gem 'truncato', '~> 0.7.9' gem 'bootstrap_form', '~> 2.7.0' -gem 'nokogiri', '~> 1.8.0' +gem 'nokogiri', '~> 1.8.1' # Diffs gem 'diffy', '~> 3.1.0' @@ -362,6 +362,7 @@ group :test do gem 'sham_rack', '~> 1.3.6' gem 'timecop', '~> 0.8.0' gem 'concurrent-ruby', '~> 1.0.5' + gem 'test-prof', '~> 0.2.5' end gem 'octokit', '~> 4.6.2' @@ -397,7 +398,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.33.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.38.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index e10db81d0c9..03ffb880fc9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -194,7 +194,7 @@ GEM factory_girl_rails (4.7.0) factory_girl (~> 4.7.0) railties (>= 3.0.0) - faraday (0.12.1) + faraday (0.12.2) multipart-post (>= 1.2, < 3) faraday_middleware (0.11.0.1) faraday (>= 0.7.4, < 1.0) @@ -275,7 +275,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.33.0) + gitaly-proto (0.38.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -293,7 +293,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab-markup (1.5.1) + gitlab-markup (1.6.2) gitlab_omniauth-ldap (2.0.4) net-ldap (~> 0.16) omniauth (~> 1.3) @@ -482,7 +482,7 @@ GEM mime-types (2.99.3) mimemagic (0.3.0) mini_mime (0.1.4) - mini_portile2 (2.2.0) + mini_portile2 (2.3.0) minitest (5.7.0) mmap2 (2.2.7) mousetrap-rails (1.4.6) @@ -496,8 +496,8 @@ GEM net-ldap (0.16.0) net-ssh (4.1.0) netrc (0.11.0) - nokogiri (1.8.0) - mini_portile2 (~> 2.2.0) + nokogiri (1.8.1) + mini_portile2 (~> 2.3.0) numerizer (0.1.1) oauth (0.5.1) oauth2 (1.4.0) @@ -516,10 +516,10 @@ GEM omniauth-oauth2 (~> 1.1) omniauth-authentiq (0.3.1) omniauth-oauth2 (~> 1.3, >= 1.3.1) - omniauth-azure-oauth2 (0.0.6) + omniauth-azure-oauth2 (0.0.9) jwt (~> 1.0) omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) + omniauth-oauth2 (~> 1.4) omniauth-cas3 (1.1.4) addressable (~> 2.3) nokogiri (~> 1.7, >= 1.7.1) @@ -545,7 +545,7 @@ GEM omniauth-oauth (1.1.0) oauth omniauth (~> 1.0) - omniauth-oauth2 (1.3.1) + omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) omniauth-oauth2-generic (0.2.2) @@ -882,6 +882,7 @@ GEM ffi sysexits (1.2.0) temple (0.7.7) + test-prof (0.2.5) test_after_commit (1.1.0) activerecord (>= 3.2) text (1.3.1) @@ -1024,10 +1025,10 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.33.0) + gitaly-proto (~> 0.38.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab-markup (~> 1.5.1) + gitlab-markup (~> 1.6.2) gitlab_omniauth-ldap (~> 2.0.4) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.4) @@ -1067,14 +1068,14 @@ DEPENDENCIES mysql2 (~> 0.4.5) net-ldap net-ssh (~> 4.1.0) - nokogiri (~> 1.8.0) + nokogiri (~> 1.8.1) oauth2 (~> 1.4) octokit (~> 4.6.2) oj (~> 2.17.4) omniauth (~> 1.4.2) omniauth-auth0 (~> 1.4.1) omniauth-authentiq (~> 0.3.1) - omniauth-azure-oauth2 (~> 0.0.6) + omniauth-azure-oauth2 (~> 0.0.9) omniauth-cas3 (~> 1.1.4) omniauth-facebook (~> 4.0.0) omniauth-github (~> 1.1.1) @@ -1163,6 +1164,7 @@ DEPENDENCIES stackprof (~> 0.2.10) state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) + test-prof (~> 0.2.5) test_after_commit (~> 1.1) thin (~> 1.7.0) timecop (~> 0.8.0) @@ -1,5 +1,7 @@ Copyright (c) 2011-2017 GitLab B.V. +With regard to the GitLab Software: + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -17,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For all third party components incorporated into the GitLab Software, those +components are licensed under the original license provided by the owner of the +applicable component.
\ No newline at end of file @@ -1 +1 @@ -9.6.0-pre +10.1.0-pre diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico Binary files differindex 156fcf07588..156fcf07588 100755..100644 --- a/app/assets/images/favicon-blue.ico +++ b/app/assets/images/favicon-blue.ico diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json new file mode 100644 index 00000000000..6b8f85e37fd --- /dev/null +++ b/app/assets/images/icons.json @@ -0,0 +1 @@ +{"iconCount":135,"spriteSize":58718,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","comment-dots","comment-next","comment","comments","commit","credit-card","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg new file mode 100644 index 00000000000..30cb2109ec2 --- /dev/null +++ b/app/assets/images/icons.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.535 7.95L1.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1 0-.266.106-.52.293-.708L5.535 7.95zm7 0L8.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1.001 0-.265.106-.519.293-.707l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 15V1a1 1 0 0 1 1-1h4.604c.93 0 1.762.088 2.495.264.733.176 1.353.445 1.863.807.509.363.897.82 1.164 1.369.268.549.401 1.197.401 1.945 0 .366-.045.718-.137 1.055-.091.337-.23.652-.417.945a3.453 3.453 0 0 1-.71.796 3.645 3.645 0 0 1-1.021.588c.469.117.87.295 1.203.533.333.238.608.515.824.83.216.315.374.657.473 1.027.099.37.148.75.148 1.138 0 1.553-.5 2.725-1.5 3.516-1 .791-2.423 1.187-4.27 1.187H3a1 1 0 0 1-1-1zm3.297-5.967v4.319H8.12c.425 0 .791-.053 1.099-.16.307-.106.564-.252.769-.44.205-.186.357-.406.456-.659.099-.252.148-.529.148-.83a3.04 3.04 0 0 0-.131-.928 1.78 1.78 0 0 0-.413-.703 1.8 1.8 0 0 0-.73-.445c-.3-.103-.66-.154-1.077-.154H5.297zm0-2.33h2.44c.842-.014 1.468-.192 1.878-.533.41-.34.616-.826.616-1.456 0-.725-.21-1.247-.632-1.566-.421-.318-1.086-.478-1.995-.478H5.297v4.033z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-additions" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.009 6.958a4 4 0 0 0 5.283 4.775 1 1 0 0 1 .712 1.87A6 6 0 0 1 2.077 6.44l-.741-.2a.5.5 0 0 1-.12-.915L3.41 4.058a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.711-1.87 6 6 0 0 1 7.927 7.162l.74.2a.5.5 0 0 1 .121.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="talic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 0h7a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm2 2h3L8 14H5L8 2zM3 14h7a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/issues.svg b/app/assets/images/illustrations/issues.svg new file mode 100644 index 00000000000..c8e0504732d --- /dev/null +++ b/app/assets/images/illustrations/issues.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.942" x="25" y="88.423" rx="2"/><mask id="h" width="25" height="8.942" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.801h43v61.603H16z"/><mask id="i" width="43" height="61.603" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.603l13.187 9.358c.449.32.876 1.015.955 1.568l3.575 24.863c.157 1.086-.253 1.257-.912.384L66 86.436l-9-6.955"/><mask id="j" width="17.75" height="36.731" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.25 60.603l13.186 9.358c.45.32.876 1.015.956 1.568l3.575 24.863c.156 1.086-.253 1.257-.912.384l-7.806-10.34-9-6.955"/><mask id="k" width="17.75" height="36.731" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.801L35.786 1.456c.947-1.357 2.48-1.36 3.428 0L59 29.8"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.265" height="35.509" x="6.367" rx="13.133"/><mask id="m" width="26.265" height="35.509" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.837" height="22.386" x="4.082" rx="8.418"/><mask id="n" width="16.837" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792 255)"><path d="M225.437 59.587c-.059.59-.132 1.27-.22 2.03a178.367 178.367 0 0 1-.965 7.07 1.5 1.5 0 1 0 2.963.465c.4-2.553.726-4.975.982-7.19a137.446 137.446 0 0 0 .297-2.832 1.5 1.5 0 1 0-2.989-.26c-.01.123-.033.365-.068.717zm-5.563 28.354a1.5 1.5 0 0 0 2.853.929c.975-2.997 1.849-6.283 2.628-9.797a1.5 1.5 0 1 0-2.928-.65c-.76 3.426-1.61 6.62-2.553 9.518zm-9.947 15.225a1.5 1.5 0 1 0 1.001 2.828c2.98-1.055 5.542-3.68 7.78-7.627a1.5 1.5 0 0 0-2.61-1.48c-1.915 3.378-3.995 5.508-6.171 6.279zm-19.488 4.417a1.5 1.5 0 1 0 1.164 2.765c3.12-1.314 6.272-2.324 9.258-2.981a1.5 1.5 0 1 0-.645-2.93c-3.167.697-6.491 1.763-9.777 3.146zm-17.208 11.043a1.5 1.5 0 0 0 2.066 2.175c2.282-2.169 4.866-4.162 7.676-5.946a1.5 1.5 0 0 0-1.608-2.533c-2.97 1.885-5.707 3.998-8.134 6.304zm-10.777 17.623a1.5 1.5 0 1 0 2.91.732c.768-3.054 2.041-5.977 3.78-8.748a1.5 1.5 0 0 0-2.54-1.595c-1.903 3.032-3.302 6.244-4.15 9.611zm-.265 20.444a1.5 1.5 0 1 0 2.977-.375c-.367-2.91-.58-6.137-.645-9.817a1.5 1.5 0 0 0-3 .053c.067 3.783.287 7.116.668 10.139zm6.219 19.472a1.5 1.5 0 0 0 2.652-1.403c-1.674-3.162-2.903-5.995-3.848-8.943a1.5 1.5 0 1 0-2.857.916c1.003 3.127 2.302 6.12 4.053 9.43zm7.566 12.77a595.837 595.837 0 0 1 2.73 4.475 1.5 1.5 0 0 0 2.569-1.551 626.463 626.463 0 0 0-2.744-4.495c.08.13-1.954-3.173-2.486-4.04a1.5 1.5 0 1 0-2.558 1.567c.534.87 2.571 4.178 2.489 4.045zm8.856 22.447a1.5 1.5 0 0 0 3-.039 32.214 32.214 0 0 0-1.837-10.326 1.5 1.5 0 0 0-2.828.999 29.212 29.212 0 0 1 1.665 9.366zm-5.483 18.028a1.5 1.5 0 0 0 2.497 1.662 36.203 36.203 0 0 0 4.488-9.416 1.5 1.5 0 0 0-2.868-.882 33.197 33.197 0 0 1-4.117 8.636z" fill="#FDE5D8"/><g transform="rotate(60 126.799 371.622)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.66m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.353" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="matrix(-1 0 0 1 18.25 0)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="34.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="40.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="46.5" cy="82.958" fill="#FC8A51" rx="1.5" ry="1.49"/><ellipse cx="37.5" cy="55.138" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.433"/><ellipse cx="37.5" cy="55.138" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.465"/></g><path fill="#EEE" d="M96.043 37.21c-.152 1.688.081 3.816.997 6.147a1.016 1.016 0 0 0 1.89-.74c-.791-2.014-.99-3.832-.865-5.226.01-.114.02-.186.024-.211a1.015 1.015 0 1 0-2.002-.333 5.06 5.06 0 0 0-.044.363zm11.487 15.683c.491.24 1.098.063 1.355-.394.257-.456.068-1.02-.424-1.26-1.866-.907-3.458-1.914-4.794-3.007a1.058 1.058 0 0 0-1.417.085.888.888 0 0 0 .091 1.317c1.458 1.192 3.183 2.283 5.19 3.26zm13.131 6.06a1.032 1.032 0 0 0 1.293-.7 1.06 1.06 0 0 0-.686-1.32 376.355 376.355 0 0 1-5.915-1.882 1.031 1.031 0 0 0-1.303.681 1.06 1.06 0 0 0 .668 1.33c1.729.569 2.905.94 5.943 1.891zm11.934 3.928c.45.246 1.022.098 1.28-.33a.872.872 0 0 0-.346-1.221c-1.494-.819-3.192-1.545-5.267-2.275-.486-.17-1.025.067-1.204.53-.18.464.07.978.555 1.149 1.984.697 3.59 1.384 4.982 2.147zm9.382 10.502c.205.494.81.742 1.349.554.54-.188.81-.74.605-1.234-.85-2.048-1.853-3.796-3.037-5.305-.337-.429-.99-.527-1.459-.218-.469.308-.575.906-.238 1.335 1.074 1.368 1.992 2.97 2.78 4.868zm2.632 13.642c.018.553.568.99 1.228.975.66-.016 1.18-.477 1.163-1.03-.073-2.204-.27-4.206-.622-6.12-.101-.547-.712-.923-1.365-.838-.652.084-1.1.597-.999 1.144.336 1.825.525 3.745.595 5.869z"/><path fill="#E5E5E5" d="M144.142 95.73a244.285 244.285 0 0 0-.142 5.254c-.007.553.396 1.008.902 1.016.506.008.923-.433.93-.985.02-1.467.056-2.681.142-5.211l.026-.767c.018-.552-.377-1.016-.882-1.036-.506-.02-.931.41-.95.963l-.026.766zm.797 19.471c.12.545.673.892 1.236.777.562-.116.921-.651.802-1.196-.417-1.9-.71-3.84-.897-5.864-.052-.554-.558-.964-1.131-.914-.573.05-.996.54-.945 1.094.195 2.102.5 4.121.935 6.103zm5.056 12.324c.296.454.953.61 1.467.348.514-.261.69-.841.395-1.295a40.725 40.725 0 0 1-2.79-4.991c-.227-.485-.855-.715-1.403-.515-.548.2-.81.755-.582 1.239a42.56 42.56 0 0 0 2.913 5.214zm4.814 7.701a33.475 33.475 0 0 0 3.543 3.531 1.021 1.021 0 0 0 1.393-.066.908.908 0 0 0-.07-1.326 31.562 31.562 0 0 1-3.34-3.328 59.092 59.092 0 0 1-.576-.682 1.02 1.02 0 0 0-1.386-.152.909.909 0 0 0-.16 1.32c.196.234.394.469.596.703zm15.825 11.677c.48.242 1.052.017 1.276-.501.224-.52.016-1.136-.464-1.378a49.756 49.756 0 0 1-4.986-2.872c-.453-.298-1.044-.144-1.32.345-.276.488-.133 1.126.32 1.424a51.568 51.568 0 0 0 5.174 2.982z"/><path fill="#EEE" d="M184.733 151.97c.553.141 1.108-.226 1.239-.82.131-.595-.21-1.192-.763-1.333a72.17 72.17 0 0 1-5.863-1.763c-.54-.188-1.12.13-1.296.712-.175.581.121 1.205.662 1.393a74.018 74.018 0 0 0 6.021 1.81zm13.2 2.028c.554.04 1.03-.445 1.065-1.083.035-.639-.386-1.188-.939-1.228a71.842 71.842 0 0 1-5.92-.676c-.55-.086-1.055.358-1.13.991-.074.634.31 1.217.86 1.303a73.28 73.28 0 0 0 6.065.693zm14.188-1.392c.55-.055.94-.457.871-.9-.068-.441-.569-.755-1.118-.7-1.917.192-3.893.32-5.91.382-.554.017-.985.392-.963.837.021.445.487.792 1.04.774a88.939 88.939 0 0 0 6.08-.393zm14.245-2.657c.53-.22.776-.816.55-1.332a1.053 1.053 0 0 0-1.367-.535 44.421 44.421 0 0 1-5.777 1.923 1.012 1.012 0 0 0-.736 1.243c.15.542.721.863 1.277.717a46.532 46.532 0 0 0 6.054-2.016zm11.483-9.532c.292-.435.148-1.006-.32-1.277-.47-.27-1.087-.138-1.379.297-.957 1.424-2.225 2.734-3.784 3.92a.88.88 0 0 0-.138 1.304c.35.396.98.453 1.408.128 1.723-1.31 3.136-2.771 4.213-4.372zm7.824-9.73a.965.965 0 0 0 .09-1.358.958.958 0 0 0-1.355-.09 44.935 44.935 0 0 0-4.17 4.163.965.965 0 0 0 .089 1.359.957.957 0 0 0 1.354-.089 43.05 43.05 0 0 1 3.991-3.985zm11.808-7.817c.476-.257.657-.858.405-1.342a.967.967 0 0 0-1.319-.412 67.097 67.097 0 0 0-5.123 3.059c-.451.298-.58.913-.287 1.373.294.46.898.59 1.35.292a65.257 65.257 0 0 1 4.974-2.97zm12.795-5.948c.55-.169.851-.724.672-1.241-.179-.518-.77-.8-1.32-.632a92.308 92.308 0 0 0-5.975 2.054c-.536.205-.794.78-.576 1.283.218.504.83.746 1.366.541a90.115 90.115 0 0 1 5.833-2.005z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.06 56s-17.47 33-12 53c5.47 20 17 32 38 44s32.44-5 60.94 6 29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108 173)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g fill="#FC8A51" transform="rotate(15 101.633 923.121)"><path d="M.398 11.298h2.388c0-4.234 3.385-7.666 7.56-7.666V1.21C4.853 1.21.399 5.727.399 11.298z"/><ellipse cx="10.745" cy="2.018" rx="1.99" ry="2.018"/></g><g fill="#FC8A51" transform="scale(-1 1) rotate(-15 -102.031 920.099)"><path d="M.398 11.298h2.388c0-4.234 3.385-7.666 7.56-7.666V1.21C4.853 1.21.399 5.727.399 11.298z"/><ellipse cx="10.745" cy="2.018" rx="1.99" ry="2.018"/></g><g transform="rotate(15 71.738 842.306)"><g fill="#FC8A51" transform="translate(29.449 11.298)"><rect width="7.959" height="2" x=".796" y="8.877" rx="1"/><rect width="7.959" height="2" x=".796" y="16.14" transform="rotate(15 4.776 17.14)" rx="1"/><rect width="7.959" height="2" x=".915" y="1.807" transform="rotate(-15 4.895 2.807)" rx="1"/></g><g fill="#FC8A51" transform="matrix(-1 0 0 1 9.551 11.298)"><rect width="7.959" height="2" x=".796" y="8.877" rx="1"/><rect width="7.959" height="2" x=".796" y="16.14" transform="rotate(15 4.776 17.14)" rx="1"/><rect width="7.959" height="2" x=".915" y="1.807" transform="rotate(-15 4.895 2.807)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.163 12.912h23.878v3H7.163z"/></g><g fill="#EEE" transform="scale(-1 1) rotate(15 -60.75 -335.206)"><path d="M.255 7.123h1.53a4.84 4.84 0 0 1 4.848-4.834V.763C3.11.763.255 3.611.255 7.123z"/><ellipse cx="6.888" cy="1.272" rx="1.276" ry="1.272"/></g><g fill="#EEE" transform="rotate(-15 60.494 -337.144)"><path d="M.255 7.123h1.53a4.84 4.84 0 0 1 4.848-4.834V.763C3.11.763.255 3.611.255 7.123z"/><ellipse cx="6.888" cy="1.272" rx="1.276" ry="1.272"/></g><g transform="scale(-1 1) rotate(15 -79.491 -386.955)"><g fill="#EEE" transform="translate(18.878 7.123)"><rect width="5.102" height="2" x=".51" y="5.596" rx="1"/><rect width="5.102" height="2" x=".51" y="10.175" transform="rotate(15 3.061 11.175)" rx="1"/><rect width="5.102" height="2" x=".587" y="1.139" transform="rotate(-15 3.138 2.14)" rx="1"/></g><g fill="#EEE" transform="matrix(-1 0 0 1 6.122 7.123)"><rect width="5.102" height="2" x=".51" y="5.596" rx="1"/><rect width="5.102" height="2" x=".51" y="10.175" transform="rotate(15 3.061 11.175)" rx="1"/><rect width="5.102" height="2" x=".587" y="1.139" transform="rotate(-15 3.138 2.14)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.592 8.14h15.306v2H4.592z"/></g><g fill="#FFF" transform="translate(0 103)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g transform="translate(39 142)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.732 13.475l-1.766-1.767a1.5 1.5 0 1 0-2.122 2.122l2.826 2.826h.001v.001c.59.59 1.535.587 2.119.003l6.37-6.37a1.504 1.504 0 0 0-.003-2.118 1.494 1.494 0 0 0-2.118-.004l-5.307 5.307z"/></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.704-7.746 11.013-20.925 4.862-31.578-7.02-12.16-22.405-16.422-34.362-9.518-11.958 6.904-15.96 22.358-8.939 34.518 6.236 10.8 19.068 15.37 30.238 11.42l10.899 18.879a4.765 4.765 0 0 0 6.508 1.748 4.768 4.768 0 0 0 1.74-6.51l-10.946-18.959zm-8.434-4.609c7.857-4.536 10.487-14.692 5.873-22.683-4.613-7.991-14.723-10.791-22.58-6.255-7.858 4.537-10.488 14.693-5.875 22.684 4.614 7.99 14.724 10.791 22.582 6.254z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/labels.svg b/app/assets/images/illustrations/labels.svg new file mode 100644 index 00000000000..3a2d521323b --- /dev/null +++ b/app/assets/images/illustrations/labels.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zm-8 0c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zm344-121l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zm9.2 164.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/icons/_merge_requests.svg b/app/assets/images/illustrations/merge_requests.svg index e77f6319a95..b9b8f0058e6 100644 --- a/app/views/shared/empty_states/icons/_merge_requests.svg +++ b/app/assets/images/illustrations/merge_requests.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/monitoring/_getting_started.svg b/app/assets/images/illustrations/monitoring/getting_started.svg index db7a1c2e708..db7a1c2e708 100644 --- a/app/views/shared/empty_states/monitoring/_getting_started.svg +++ b/app/assets/images/illustrations/monitoring/getting_started.svg diff --git a/app/views/shared/empty_states/monitoring/_loading.svg b/app/assets/images/illustrations/monitoring/loading.svg index 6bbd7a6c5b9..6bbd7a6c5b9 100644 --- a/app/views/shared/empty_states/monitoring/_loading.svg +++ b/app/assets/images/illustrations/monitoring/loading.svg diff --git a/app/views/shared/empty_states/monitoring/_unable_to_connect.svg b/app/assets/images/illustrations/monitoring/unable_to_connect.svg index 62537d87d5d..62537d87d5d 100644 --- a/app/views/shared/empty_states/monitoring/_unable_to_connect.svg +++ b/app/assets/images/illustrations/monitoring/unable_to_connect.svg diff --git a/app/assets/images/illustrations/pipelines_empty.svg b/app/assets/images/illustrations/pipelines_empty.svg new file mode 100644 index 00000000000..f3107c8f062 --- /dev/null +++ b/app/assets/images/illustrations/pipelines_empty.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd"><g fill="#e5e5e5" transform="translate(0 102)"><rect width="74" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="M152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 152 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 166 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 180 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 194 23m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4A1.994 1.994 0 0 1 208 23"/></g><g fill="#31af64"><path fill-rule="nonzero" d="M19 144c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="M17.07 127.02l-2.829-2.829a1.995 1.995 0 0 0-2.828 0 1.995 1.995 0 0 0 0 2.828l4.243 4.243a1.995 1.995 0 0 0 2.822.006l7.79-7.79a1.997 1.997 0 0 0-.006-2.823 1.992 1.992 0 0 0-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a"><path fill-rule="nonzero" d="M126 149.5c-12.979 0-23.5-10.521-23.5-23.5s10.521-23.5 23.5-23.5 23.5 10.521 23.5 23.5-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5s-8.283-18.5-18.5-18.5-18.5 8.283-18.5 18.5 8.283 18.5 18.5 18.5"/><path d="M130.24 126l2.833-2.833a3 3 0 0 0-4.243-4.243l-2.833 2.833-2.833-2.833a3 3 0 0 0-4.243 4.243l2.833 2.833-2.833 2.833a3 3 0 0 0 4.243 4.243l2.833-2.833 2.833 2.833a3 3 0 0 0 4.243-4.243L130.24 126"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="M236 139c-7.732 0-14-6.268-14-14s6.268-14 14-14 14 6.268 14 14-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10"/><g transform="translate(73 4)"><path stroke="#e5e5e5" stroke-width="4" d="M64.82 76H98c4.419 0 8-3.579 8-7.99V7.99C106 3.577 102.417 0 98 0H8.009c-4.419 0-8 3.579-8 7.99v60.02c0 4.413 3.583 7.99 8 7.99h31.935l9.263 9.855a4.357 4.357 0 0 0 6.354 0L64.824 76"/><rect width="18" height="6" x="11" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="35" y="35" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="29" y="51" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="35" y="19" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="53" y="51" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="11" y="51" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="77" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="11" y="35" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="53" y="19" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="65" y="19" rx="3"/><rect width="6" height="6" x="71" y="35" rx="3"/></g><rect width="6" height="6" x="59" y="35" fill="#e52c5a" rx="3"/></g><path fill="#6b4fbb" fill-rule="nonzero" d="M151.869 77.403c-13.26 9.264-31.649 7.977-43.484-3.858-13.279-13.279-13.279-34.806 0-48.084 13.278-13.278 34.805-13.278 48.083 0 11.836 11.836 13.118 30.23 3.858 43.485.133.111.262.229.387.354l15.556 15.555a6.004 6.004 0 0 1 0 8.486 5.997 5.997 0 0 1-8.486 0l-15.555-15.556a6.051 6.051 0 0 1-.355-.387m-1.06-9.512c10.154-10.154 10.154-26.617 0-36.77-10.153-10.154-26.616-10.154-36.77 0-10.153 10.153-10.153 26.616 0 36.77 10.154 10.153 26.617 10.153 36.77 0"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/pipelines_failed.svg b/app/assets/images/illustrations/pipelines_failed.svg new file mode 100644 index 00000000000..8daf7da86ed --- /dev/null +++ b/app/assets/images/illustrations/pipelines_failed.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12a36.87 36.87 0 0 0-15.32 3.311c-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088C145.95 45.083 134 59.645 134 76.996c0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="b" cx="41" cy="41" rx="41" ry="41"/><mask id="c" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="d" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd"><path stroke="#b5a7dd" stroke-width="4" d="M228.415 137.792c8.443 17.156 21.89 32.082 39.688 42.358"/><path fill="#fb722e" d="M284.464 183.822a2.006 2.006 0 0 1 2.74-.727l6.914 3.992a2.001 2.001 0 0 1 .741 2.737 2.006 2.006 0 0 1-2.74.727l-6.914-3.992a2.001 2.001 0 0 1-.74-2.737m-5 8.66a2.006 2.006 0 0 1 2.74-.726l6.913 3.991a2.001 2.001 0 0 1 .741 2.737 2.006 2.006 0 0 1-2.74.727l-6.914-3.991a2.001 2.001 0 0 1-.74-2.737"/><path fill="#fde5d8" fill-rule="nonzero" d="M267.072 189.947l5.196 3a5.998 5.998 0 0 0 8.195-2.194l3.005-5.205a5.995 5.995 0 0 0-2.198-8.193l-5.196-3-9 15.588m6.032-18.447a3.005 3.005 0 0 1 4.098-1.11l6.07 3.505c4.784 2.761 6.426 8.871 3.662 13.658l-3.005 5.204c-2.76 4.782-8.875 6.42-13.659 3.658l-6.07-3.505a2.999 2.999 0 0 1-1.088-4.104l9.992-17.306"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="M260.597 18.747C266.208 9.657 276.116 4 287 4c17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466A34.855 34.855 0 0 0 322 35.001c0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><path fill="#b5a7dd" d="M281.982 23.991l-2.526 1.154-2.992-2.993a.4.4 0 0 0-.564.009l-1.738 1.738a.392.392 0 0 0-.009.564l2.987 2.987-1.147 2.524a12.26 12.26 0 0 0-1.04 3.883l-.269 2.76-4.08 1.093a.399.399 0 0 0-.275.492l.636 2.375c.06.223.273.346.485.29l4.087-1.096 1.611 2.262a12.017 12.017 0 0 0 2.827 2.828l2.26 1.612-1.094 4.08a.399.399 0 0 0 .29.485l2.374.636a.393.393 0 0 0 .493-.275l1.093-4.08 2.763-.267a12.14 12.14 0 0 0 3.862-1.035l2.526-1.154 2.992 2.992a.4.4 0 0 0 .564-.008l1.738-1.738a.392.392 0 0 0 .009-.564l-2.987-2.987 1.147-2.524a12.26 12.26 0 0 0 1.04-3.883l.27-2.76 4.08-1.093a.399.399 0 0 0 .274-.493l-.636-2.374a.393.393 0 0 0-.485-.29l-4.087 1.096-1.611-2.262a12.017 12.017 0 0 0-2.826-2.828l-2.26-1.612 1.093-4.08a.399.399 0 0 0-.29-.485l-2.373-.636a.393.393 0 0 0-.493.274l-1.094 4.081-2.763.266c-1.336.129-2.64.48-3.862 1.036m3.48-5.02l.375-1.4a4.393 4.393 0 0 1 5.392-3.103l2.375.636a4.399 4.399 0 0 1 3.117 5.383l-.375 1.401a16.077 16.077 0 0 1 3.761 3.767l1.405-.376a4.397 4.397 0 0 1 5.386 3.118l.636 2.375a4.398 4.398 0 0 1-3.103 5.39l-1.402.376a16.217 16.217 0 0 1-1.378 5.143l1.027 1.026a4.392 4.392 0 0 1-.008 6.22l-1.739 1.738a4.4 4.4 0 0 1-6.224.008l-1.028-1.028a16.09 16.09 0 0 1-5.14 1.381l-.376 1.4a4.393 4.393 0 0 1-5.392 3.104l-2.374-.636a4.399 4.399 0 0 1-3.118-5.383l.376-1.401a16.077 16.077 0 0 1-3.762-3.767l-1.404.376a4.397 4.397 0 0 1-5.386-3.118l-.637-2.374a4.398 4.398 0 0 1 3.103-5.391l1.402-.376a16.217 16.217 0 0 1 1.378-5.143l-1.026-1.026a4.392 4.392 0 0 1 .008-6.22l1.738-1.738a4.4 4.4 0 0 1 6.224-.008l1.028 1.028a16.09 16.09 0 0 1 5.141-1.381"/><path fill="#6b4fbb" d="M286.367 37.355a2.439 2.439 0 1 0 1.262-4.711 2.439 2.439 0 0 0-1.262 4.711m-1.035 3.864a6.44 6.44 0 1 1 3.333-12.44 6.44 6.44 0 0 1-3.333 12.44"/></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#c)" stroke-linejoin="round" xlink:href="#a"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#d)" xlink:href="#b"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="M41 78c20.435 0 37-16.565 37-37S61.435 4 41 4 4 20.565 4 41s16.565 37 37 37m0 4C18.356 82 0 63.644 0 41S18.356 0 41 0s41 18.356 41 41-18.356 41-41 41"/><path fill="#b5a7dd" d="M34.363 26.44l-2.527 1.154-3.211-3.211a1.495 1.495 0 0 0-2.117-.005l-2.131 2.13a1.504 1.504 0 0 0 .005 2.117l3.206 3.206-1.147 2.524a16.09 16.09 0 0 0-.897 2.503 16.08 16.08 0 0 0-.475 2.616l-.269 2.76-4.379 1.174a1.495 1.495 0 0 0-1.063 1.83l.78 2.911a1.504 1.504 0 0 0 1.836 1.054l4.387-1.176 1.612 2.263a15.954 15.954 0 0 0 3.737 3.742l2.26 1.612-1.173 4.38a1.495 1.495 0 0 0 1.053 1.835l2.908.78a1.504 1.504 0 0 0 1.83-1.063l1.174-4.38 2.763-.266a15.977 15.977 0 0 0 5.108-1.372l2.527-1.154 3.211 3.212a1.495 1.495 0 0 0 2.117.005l2.131-2.131a1.504 1.504 0 0 0-.005-2.117l-3.206-3.206 1.147-2.524a16.09 16.09 0 0 0 .897-2.503 16.1 16.1 0 0 0 .475-2.616l.269-2.76 4.379-1.173a1.495 1.495 0 0 0 1.063-1.83l-.78-2.912a1.504 1.504 0 0 0-1.836-1.054l-4.387 1.176-1.612-2.262a15.954 15.954 0 0 0-3.737-3.743l-2.26-1.612 1.173-4.38a1.495 1.495 0 0 0-1.053-1.835l-2.908-.779a1.504 1.504 0 0 0-1.83 1.063l-1.174 4.38-2.763.265c-1.767.17-3.493.636-5.108 1.373m4.726-5.355l.455-1.699a5.504 5.504 0 0 1 6.73-3.89l2.907.778a5.495 5.495 0 0 1 3.882 6.735l-.455 1.699a19.95 19.95 0 0 1 4.673 4.68l1.704-.457a5.503 5.503 0 0 1 6.734 3.886l.78 2.91a5.493 5.493 0 0 1-3.894 6.73l-1.701.455a20.134 20.134 0 0 1-.593 3.265 20.134 20.134 0 0 1-1.119 3.124l1.245 1.246a5.507 5.507 0 0 1 .008 7.774l-2.13 2.13a5.5 5.5 0 0 1-7.775-.001l-1.248-1.248c-2 .914-4.157 1.502-6.387 1.717l-.455 1.699a5.504 5.504 0 0 1-6.73 3.89l-2.907-.778a5.495 5.495 0 0 1-3.882-6.735l.455-1.699a19.95 19.95 0 0 1-4.673-4.68l-1.704.457a5.503 5.503 0 0 1-6.734-3.886l-.78-2.91a5.493 5.493 0 0 1 3.894-6.73l1.701-.455a20.258 20.258 0 0 1 1.712-6.389l-1.245-1.246a5.507 5.507 0 0 1-.008-7.774l2.13-2.13a5.5 5.5 0 0 1 7.775.001l1.248 1.248c2-.914 4.157-1.502 6.387-1.717"/><path fill="#6b4fbb" d="M39.965 44.863a4 4 0 1 0 2.07-7.727 4 4 0 0 0-2.07 7.727m-1.036 3.864a8 8 0 1 1 4.142-15.455 8 8 0 0 1-4.142 15.455"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="M144 169.541v30.01a4.002 4.002 0 0 0 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01a4.002 4.002 0 0 0-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e" transform="translate(140 161)"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="M445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364a2 2 0 1 0 2.3 3.272c15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617a2.001 2.001 0 0 0 2.29-3.28"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/icons/_priority_labels.svg b/app/assets/images/illustrations/priority_labels.svg index 7282c2b215e..b79c551d3d7 100644 --- a/app/views/shared/empty_states/icons/_priority_labels.svg +++ b/app/assets/images/illustrations/priority_labels.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zM10.4 9.2C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg> +<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zm5-34C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/todos_all_done.svg b/app/assets/images/illustrations/todos_all_done.svg new file mode 100644 index 00000000000..6387497a6fb --- /dev/null +++ b/app/assets/images/illustrations/todos_all_done.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/todos_empty.svg b/app/assets/images/illustrations/todos_empty.svg new file mode 100644 index 00000000000..4de6cb403b9 --- /dev/null +++ b/app/assets/images/illustrations/todos_empty.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/><mask id="l" width="180" height="220" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><rect id="b" width="180" height="220" rx="6"/><mask id="m" width="180" height="220" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><rect id="c" width="28" height="28" rx="4"/><mask id="n" width="28" height="28" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><rect id="d" width="28" height="28" rx="4"/><mask id="o" width="28" height="28" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><circle id="e" cx="21.5" cy="21.5" r="21.5"/><mask id="p" width="43" height="43" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><circle id="f" cx="26.5" cy="26.5" r="26.5"/><mask id="q" width="53" height="53" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><circle id="g" cx="9.5" cy="4.5" r="4.5"/><mask id="r" width="13" height="13" x="-2" y="-2"><path fill="#fff" d="M3-2h13v13H3z"/><use xlink:href="#g"/></mask><circle id="h" cx="26.5" cy="26.5" r="26.5"/><mask id="s" width="53" height="53" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask><circle id="i" cx="21.5" cy="21.5" r="21.5"/><mask id="t" width="43" height="43" x="0" y="0" fill="#fff"><use xlink:href="#i"/></mask><path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/><mask id="u" width="52" height="44" x="0" y="0" fill="#fff"><use xlink:href="#j"/></mask><circle id="k" cx="18.5" cy="18.5" r="18.5"/><mask id="v" width="37" height="37" x="0" y="0" fill="#fff"><use xlink:href="#k"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(-6 -4)"><use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/><g transform="rotate(5 -707.333 618.042)"><use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/><g transform="translate(29 24)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(29 69)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(28 160)"><use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/><rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/><rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/></g><g transform="translate(28 116)"><use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/><rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/><rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/></g></g><g transform="rotate(-15 601.917 -782.362)"><use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/><text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1"><tspan x="12" y="27">@</tspan></text></g><g transform="rotate(15 -686.59 1035.907)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/><path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/><g transform="translate(17 14)"><use fill="#FC6D26" xlink:href="#g"/><use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/></g></g><g transform="rotate(15 -85.125 65.185)"><use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/><path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/></g><g transform="rotate(-15 716.492 78.873)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/><path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/></g><g transform="rotate(-15 129.114 -585.74)"><use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/><circle cx="16" cy="20" r="2" fill="#FC6D26"/><circle cx="27" cy="20" r="2" fill="#FC6D26"/><circle cx="38" cy="20" r="2" fill="#FC6D26"/></g><g transform="rotate(-15 1254.8 -458.986)"><use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/><path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/><rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 22fa1f2a609..4f01345ee3b 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,6 +2,7 @@ /* global Flash */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -23,6 +24,9 @@ const categoryLabelMap = { flags: 'Flags', }; +const IS_VISIBLE = 'is-visible'; +const IS_RENDERED = 'is-rendered'; + class AwardsHandler { constructor(emoji) { this.emoji = emoji; @@ -50,7 +54,7 @@ class AwardsHandler { if (!$target.closest('.emoji-menu').length) { if ($('.emoji-menu').is(':visible')) { $('.js-add-award.is-active').removeClass('is-active'); - $('.emoji-menu').removeClass('is-visible'); + this.hideMenuElement($('.emoji-menu')); } } }); @@ -87,12 +91,12 @@ class AwardsHandler { if ($menu.length) { if ($menu.is('.is-visible')) { $addBtn.removeClass('is-active'); - $menu.removeClass('is-visible'); + this.hideMenuElement($menu); $('.js-emoji-menu-search').blur(); } else { $addBtn.addClass('is-active'); this.positionMenu($menu, $addBtn); - $menu.addClass('is-visible'); + this.showMenuElement($menu); $('.js-emoji-menu-search').focus(); } } else { @@ -102,7 +106,7 @@ class AwardsHandler { $addBtn.removeClass('is-loading'); this.positionMenu($createdMenu, $addBtn); return setTimeout(() => { - $createdMenu.addClass('is-visible'); + this.showMenuElement($createdMenu); $('.js-emoji-menu-search').focus(); }, 200); }); @@ -237,10 +241,11 @@ class AwardsHandler { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; - if (gl.utils.isInIssuePage() && !isMainAwardsBlock) { + if (isInIssuePage() && !isMainAwardsBlock) { const id = votesBlock.attr('id').replace('note_', ''); - $('.emoji-menu').removeClass('is-visible'); + this.hideMenuElement($('.emoji-menu')); + $('.js-add-award.is-active').removeClass('is-active'); const toggleAwardEvent = new CustomEvent('toggleAward', { detail: { @@ -260,7 +265,8 @@ class AwardsHandler { return typeof callback === 'function' ? callback() : undefined; }); - $('.emoji-menu').removeClass('is-visible'); + this.hideMenuElement($('.emoji-menu')); + return $('.js-add-award.is-active').removeClass('is-active'); } @@ -288,7 +294,7 @@ class AwardsHandler { } getVotesBlock() { - if (gl.utils.isInIssuePage()) { + if (isInIssuePage()) { const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); if ($el.length) { @@ -452,11 +458,11 @@ class AwardsHandler { userAuthored($emojiButton) { const oldTitle = this.getAwardTooltip($emojiButton); const newTitle = 'You cannot vote on your own issue, MR and note'; - gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show'); + updateTooltipTitle($emojiButton, newTitle).tooltip('show'); // Restore tooltip back to award list return setTimeout(() => { $emojiButton.tooltip('hide'); - gl.utils.updateTooltipTitle($emojiButton, oldTitle); + updateTooltipTitle($emojiButton, oldTitle); }, 2800); } @@ -528,6 +534,33 @@ class AwardsHandler { return $matchingElements.closest('li').clone(); } + /* showMenuElement and hideMenuElement are performance optimizations. We use + * opacity to show/hide the emoji menu, because we can animate it. But opacity + * leaves hidden elements in the render tree, which is unacceptable given the number + * of emoji elements in the emoji menu (5k+). To get the best of both worlds, we separately + * apply IS_RENDERED to add/remove the menu from the render tree and IS_VISIBLE to animate + * the menu being opened and closed. */ + + showMenuElement($emojiMenu) { + $emojiMenu.addClass(IS_RENDERED); + + // enqueues animation as a microtask, so it begins ASAP once IS_RENDERED added + return Promise.resolve() + .then(() => $emojiMenu.addClass(IS_VISIBLE)); + } + + hideMenuElement($emojiMenu) { + $emojiMenu.on(transitionEndEventString, (e) => { + if (e.currentTarget === e.target) { + $emojiMenu + .removeClass(IS_RENDERED) + .off(transitionEndEventString); + } + }); + + $emojiMenu.removeClass(IS_VISIBLE); + } + destroy() { this.eventListeners.forEach((entry) => { entry.element.off.call(entry.element, ...entry.args); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 79702c54852..2cf8f4fa935 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,5 @@ import '../commons/bootstrap'; +import { isInIssuePage } from '../lib/utils/common_utils'; // Quick Submit behavior // @@ -45,7 +46,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { if (!$submitButton.attr('disabled')) { $submitButton.trigger('click', [e]); - if (!gl.utils.isInIssuePage()) { + if (!isInIssuePage()) { $submitButton.disable(); } } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 26d3419a162..ddd1fea3aca 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -3,6 +3,7 @@ import '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; +import csrf from '../lib/utils/csrf'; function toggleLoading($el, $icon, loading) { if (loading) { @@ -36,9 +37,7 @@ export default class BlobFileDropzone { maxFiles: 1, addRemoveLinks: true, previewsContainer: '.dropzone-previews', - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'), - }, + headers: csrf.headers, init: function () { this.on('addedfile', function () { toggleLoading(submitButton, submitButtonLoadingIcon, false); diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js index 27312d718b0..c858a6bb7b4 100644 --- a/app/assets/javascripts/blob/notebook/index.js +++ b/app/assets/javascripts/blob/notebook/index.js @@ -40,10 +40,10 @@ export default () => { class="text-center" v-if="error"> <span v-if="loadError"> - An error occured whilst loading the file. Please try again later. + An error occurred whilst loading the file. Please try again later. </span> <span v-else> - An error occured whilst parsing the file. + An error occurred whilst parsing the file. </span> </p> </div> diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index 0ed915c1ac9..7109f356540 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -48,10 +48,10 @@ export default () => { class="text-center" v-if="error"> <span v-if="loadError"> - An error occured whilst loading the file. Please try again later. + An error occurred whilst loading the file. Please try again later. </span> <span v-else> - An error occured whilst decoding the file. + An error occurred whilst decoding the file. </span> </p> </div> diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 187fab084fd..e0b73f13d36 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,4 +1,6 @@ /* global Flash */ +import { handleLocationHash } from '../../lib/utils/common_utils'; + export default class BlobViewer { constructor() { BlobViewer.initAuxiliaryViewer(); @@ -114,7 +116,7 @@ export default class BlobViewer { $(viewer).renderGFM(); this.$fileHolder.trigger('highlight:line'); - gl.utils.handleLocationHash(); + handleLocationHash(); this.toggleCopyButtonState(); }) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index ea00efe4b46..815248f38ee 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -77,9 +77,6 @@ $(() => { }); Store.rootPath = this.boardsEndpoint; - this.filterManager = new FilteredSearchBoards(Store.filter, true); - this.filterManager.setup(); - // Listen for updateTokens event eventHub.$on('updateTokens', this.updateTokens); }, @@ -87,6 +84,9 @@ $(() => { eventHub.$off('updateTokens', this.updateTokens); }, mounted () { + this.filterManager = new FilteredSearchBoards(Store.filter, true); + this.filterManager.setup(); + Store.disabled = this.disabled; gl.boardService.all() .then(response => response.json()) diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js index 541b8049855..bc28f7f45f4 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -68,7 +68,7 @@ export default { <div class="flash-container" v-if="error"> <div class="flash-alert"> - An error occured. Please try again. + An error occurred. Please try again. </div> </div> <label class="label-light" diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index 13569df0c20..e571b11a83d 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -8,11 +8,11 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ return ModalStore.store; }, props: { - image: { + newIssuePath: { type: String, required: true, }, - newIssuePath: { + emptyStateSvg: { type: String, required: true, }, @@ -42,7 +42,7 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ <section class="empty-state"> <div class="row"> <div class="col-xs-12 col-sm-6 col-sm-push-6"> - <aside class="svg-content" v-html="image"></aside> + <aside class="svg-content"><img :src="emptyStateSvg"/></aside> </div> <div class="col-xs-12 col-sm-6 col-sm-pull-6"> <div class="text-content"> diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 96af69e7a36..d2044f20ebe 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -12,11 +12,11 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.IssuesModal = Vue.extend({ props: { - blankStateImage: { + newIssuePath: { type: String, required: true, }, - newIssuePath: { + emptyStateSvg: { type: String, required: true, }, @@ -150,14 +150,14 @@ gl.issueBoards.IssuesModal = Vue.extend({ :label-path="labelPath"> </modal-header> <modal-list - :image="blankStateImage" :issue-link-base="issueLinkBase" :root-path="rootPath" + :empty-state-svg="emptyStateSvg" v-if="!loading && showList && !filterLoading"></modal-list> <empty-state v-if="showEmptyState" - :image="blankStateImage" - :new-issue-path="newIssuePath"></empty-state> + :new-issue-path="newIssuePath" + :empty-state-svg="emptyStateSvg"></empty-state> <section class="add-issues-list text-center" v-if="loading || filterLoading"> diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index b4a45feee4d..7c62134b3a3 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -15,7 +15,7 @@ gl.issueBoards.ModalList = Vue.extend({ type: String, required: true, }, - image: { + emptyStateSvg: { type: String, required: true, }, @@ -119,8 +119,8 @@ gl.issueBoards.ModalList = Vue.extend({ class="empty-state add-issues-empty-state-filter text-center" v-if="issuesCount > 0 && issues.length === 0"> <div - class="svg-content" - v-html="image"> + class="svg-content"> + <img :src="emptyStateSvg"/> </div> <div class="text-content"> <h4> diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 43928e602d6..ea82958e80d 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -2,6 +2,7 @@ /* global List */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { getUrlParamsArray } from '../../lib/utils/common_utils'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -21,7 +22,7 @@ gl.issueBoards.BoardsStore = { }, create () { this.state.lists = []; - this.filter.path = gl.utils.getUrlParamsArray().join('&'); + this.filter.path = getUrlParamsArray().join('&'); this.detail = { issue: {} }; }, addList (listObj, defaultAvatar) { diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index ae1a23132a7..3d27a3544eb 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -3,6 +3,7 @@ consistent-return, prefer-rest-params */ import _ from 'underscore'; import bp from './breakpoints'; import { bytesToKiB } from './lib/utils/number_utils'; +import { setCiStatusFavicon } from './lib/utils/common_utils'; window.Build = (function () { Build.timeout = null; @@ -166,10 +167,10 @@ window.Build = (function () { Build.prototype.getBuildTrace = function () { return $.ajax({ url: `${this.pageUrl}/trace.json`, - data: this.state, + data: { state: this.state }, }) .done((log) => { - gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); + setCiStatusFavicon(`${this.pageUrl}/status.json`); if (log.state) { this.state = log.state; diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 17d14dc1e79..4763985c802 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -11,14 +11,22 @@ function ImageFile(file) { this.file = file; this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { - // Determine if old and new file has same dimensions, if not show 'two-up' view return function(deletedWidth, deletedHeight) { return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { - if (width === deletedWidth && height === deletedHeight) { - return _this.initViewModes(); - } else { - return _this.initView('two-up'); - } + _this.initViewModes(); + + // Load two-up view after images are loaded + // so that we can display the correct width and height information + const images = $('.two-up.view img', _this.file); + let loadedCount = 0; + + images.on('load', () => { + loadedCount += 1; + + if (loadedCount === images.length) { + _this.initView('two-up'); + } + }); }); }; })(this)); @@ -134,8 +142,9 @@ width: maxWidth + 1, height: maxHeight + 2 }); + // Set swipeBar left position to match image frame $swipeBar.css({ - left: 0 + left: 1 }); wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 16c5d0fa344..1f9153d95bd 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -35,6 +35,8 @@ document.addEventListener('DOMContentLoaded', () => { propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, + errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index c931e1e0ea5..0661087a1ba 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -17,6 +17,14 @@ type: String, required: true, }, + emptyStateSvgPath: { + type: String, + required: true, + }, + errorStateSvgPath: { + type: String, + required: true, + }, }, mixins: [ pipelinesMixin, @@ -87,10 +95,12 @@ <empty-state v-if="shouldRenderEmptyState" :help-page-path="helpPagePath" + :empty-state-svg-path="emptyStateSvgPath" /> <error-state v-if="shouldRenderErrorState" + :error-state-svg-path="errorStateSvgPath" /> <div diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index b375b61202e..eae4a7eab55 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */ +import { rstrip } from './lib/utils/common_utils'; window.ConfirmDangerModal = (function() { function ConfirmDangerModal(form, text) { @@ -12,7 +13,7 @@ window.ConfirmDangerModal = (function() { submit.disable(); $('.js-confirm-danger-input').off('input'); $('.js-confirm-danger-input').on('input', function() { - if (gl.utils.rstrip($(this).val()) === project_path) { + if (rstrip($(this).val()) === project_path) { return submit.enable(); } else { return submit.disable(); diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 13ba4a57293..e3e2c798570 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ import _ from 'underscore'; -import './lib/utils/common_utils'; +import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils'; import { placeholderImage } from './lazy_loader'; const gfmRules = { @@ -295,7 +295,7 @@ class CopyAsGFM { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; - const documentFragment = window.gl.utils.getSelectedFragment(); + const documentFragment = getSelectedFragment(); if (!documentFragment) return; const el = transformer(documentFragment.cloneNode(true)); @@ -412,7 +412,7 @@ class CopyAsGFM { for (const selector in rules) { const func = rules[selector]; - if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; + if (!nodeMatchesSelector(node, selector)) continue; let result; if (func.length === 2) { diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js deleted file mode 100644 index 8d3d34f836f..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js +++ /dev/null @@ -1,17 +0,0 @@ -export default { - props: { - count: { - type: Number, - required: true, - }, - }, - template: ` - <span v-if="count === 50" class="events-info pull-right"> - <i class="fa fa-warning has-tooltip" - aria-hidden="true" - :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)" - data-placement="top"></i> - {{ n__('Showing %d event', 'Showing %d events', 50) }} - </span> - `, -}; diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue new file mode 100644 index 00000000000..6e94ba929b2 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -0,0 +1,26 @@ +<script> + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + props: { + count: { + type: Number, + required: true, + }, + }, + directives: { + tooltip, + }, + }; +</script> +<template> + <span v-if="count === 50" class="events-info pull-right"> + <i + class="fa fa-warning" + v-tooltip + aria-hidden="true" + :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)" + data-placement="top"></i> + {{ n__('Showing %d event', 'Showing %d events', 50) }} + </span> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js deleted file mode 100644 index 7c32a38fbe7..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable no-param-reassign */ - -import Vue from 'vue'; -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.StageCodeComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - components: { - userAvatarImage, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="mergeRequest in items" class="stage-event-item"> - <div class="item-details"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> - <h5 class="item-title merge-merquest-title"> - <a :href="mergeRequest.url"> - {{ mergeRequest.title }} - </a> - </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> - · - <span> - {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> - </span> - <span> - {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> - </span> - </div> - <div class="item-time"> - <total-time :time="mergeRequest.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue new file mode 100644 index 00000000000..e4d62b649e5 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue @@ -0,0 +1,47 @@ +<script> + import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; + + export default { + props: { + items: Array, + stage: Object, + }, + components: { + userAvatarImage, + }, + }; +</script> +<template> + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> + </div> + <ul class="stage-event-list"> + <li v-for="mergeRequest in items" class="stage-event-item"> + <div class="item-details"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + {{ s__('OpenedNDaysAgo|Opened') }} + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + {{ s__('ByAuthor|by') }} + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"></total-time> + </div> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue new file mode 100644 index 00000000000..ab730af8f5b --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue @@ -0,0 +1,53 @@ +<script> + import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; + + export default { + props: { + items: Array, + stage: Object, + }, + components: { + userAvatarImage, + }, + }; +</script> +<template> + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> + </div> + <ul class="stage-event-list"> + <li + v-for="(issue, i) in items" + :key="i" + class="stage-event-item"> + <div class="item-details"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="issue.author.avatarUrl"/> + <h5 class="item-title issue-title"> + <a class="issue-title" :href="issue.url"> + {{ issue.title }} + </a> + </h5> + <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + · + <span> + {{ s__('OpenedNDaysAgo|Opened') }} + <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + </span> + <span> + {{ s__('ByAuthor|by') }} + <a :href="issue.author.webUrl" class="issue-author-link"> + {{ issue.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="issue.totalTime"/> + </div> + </li> + </ul> + </div> +</template> + diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js deleted file mode 100644 index 5f4a0ac8590..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable no-param-reassign */ -import Vue from 'vue'; -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.StageIssueComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - components: { - userAvatarImage, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="issue in items" class="stage-event-item"> - <div class="item-details"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="issue.author.avatarUrl"/> - <h5 class="item-title issue-title"> - <a class="issue-title" :href="issue.url"> - {{ issue.title }} - </a> - </h5> - <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> - · - <span> - {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> - </span> - <span> - {{ s__('ByAuthor|by') }} - <a :href="issue.author.webUrl" class="issue-author-link"> - {{ issue.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="issue.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js deleted file mode 100644 index 11fee5410d9..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable no-param-reassign */ -import Vue from 'vue'; -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; -import iconCommit from '../svg/icon_commit.svg'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.StagePlanComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - components: { - userAvatarImage, - }, - data() { - return { iconCommit }; - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="commit in items" class="stage-event-item"> - <div class="item-details item-conmmit-component"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="commit.author.avatarUrl"/> - <h5 class="item-title commit-title"> - <a :href="commit.commitUrl"> - {{ commit.title }} - </a> - </h5> - <span> - {{ s__('FirstPushedBy|First') }} - <span class="commit-icon">${iconCommit}</span> - <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> - {{ s__('FirstPushedBy|pushed by') }} - <a :href="commit.author.webUrl" class="commit-author-link"> - {{ commit.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="commit.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue new file mode 100644 index 00000000000..152c086a606 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue @@ -0,0 +1,56 @@ +<script> +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; +import iconCommit from '../svg/icon_commit.svg'; + +export default { + props: { + items: Array, + stage: Object, + }, + components: { + userAvatarImage, + }, + computed: { + iconCommit() { + return iconCommit; + }, + }, +}; +</script> +<template> + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> + </div> + <ul class="stage-event-list"> + <li + v-for="(commit, i) in items" + :key="i" + class="stage-event-item"> + <div class="item-details item-conmmit-component"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="commit.author.avatarUrl"/> + <h5 class="item-title commit-title"> + <a :href="commit.commitUrl"> + {{ commit.title }} + </a> + </h5> + <span> + {{ s__('FirstPushedBy|First') }} + <span class="commit-icon" v-html="iconCommit"></span> + <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> + {{ s__('FirstPushedBy|pushed by') }} + <a :href="commit.author.webUrl" class="commit-author-link"> + {{ commit.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="commit.totalTime" /> + </div> + </li> + </ul> + </div> +</template> + diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js deleted file mode 100644 index b7ba9360f70..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable no-param-reassign */ -import Vue from 'vue'; -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.StageProductionComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - components: { - userAvatarImage, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="issue in items" class="stage-event-item"> - <div class="item-details"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="issue.author.avatarUrl"/> - <h5 class="item-title issue-title"> - <a class="issue-title" :href="issue.url"> - {{ issue.title }} - </a> - </h5> - <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> - · - <span> - {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> - </span> - <span> - {{ s__('ByAuthor|by') }} - <a :href="issue.author.webUrl" class="issue-author-link"> - {{ issue.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="issue.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js deleted file mode 100644 index f41a0d0e4ff..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable no-param-reassign */ -import Vue from 'vue'; -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.StageReviewComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - components: { - userAvatarImage, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="mergeRequest in items" class="stage-event-item"> - <div class="item-details"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> - <h5 class="item-title merge-merquest-title"> - <a :href="mergeRequest.url"> - {{ mergeRequest.title }} - </a> - </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> - · - <span> - {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> - </span> - <span> - {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> - </span> - <template v-if="mergeRequest.state === 'closed'"> - <span class="merge-request-state"> - <i class="fa fa-ban"></i> - {{ mergeRequest.state.toUpperCase() }} - </span> - </template> - <template v-else> - <span class="merge-request-branch" v-if="mergeRequest.branch"> - <i class= "fa fa-code-fork"></i> - <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> - </span> - </template> - </div> - <div class="item-time"> - <total-time :time="mergeRequest.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue new file mode 100644 index 00000000000..9e66b690404 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -0,0 +1,62 @@ +<script> + import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; + + export default { + props: { + items: Array, + stage: Object, + }, + components: { + userAvatarImage, + }, + }; +</script> +<template> + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> + </div> + <ul class="stage-event-list"> + <li + v-for="(mergeRequest, i) in items" + :key="i" + class="stage-event-item"> + <div class="item-details"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> + <h5 class="item-title merge-merquest-title"> + <a :href="mergeRequest.url"> + {{ mergeRequest.title }} + </a> + </h5> + <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + · + <span> + {{ s__('OpenedNDaysAgo|Opened') }} + <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + </span> + <span> + {{ s__('ByAuthor|by') }} + <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + </span> + <template v-if="mergeRequest.state === 'closed'"> + <span class="merge-request-state"> + <i class="fa fa-ban"></i> + {{ mergeRequest.state.toUpperCase() }} + </span> + </template> + <template v-else> + <span class="merge-request-branch" v-if="mergeRequest.branch"> + <i class= "fa fa-code-fork"></i> + <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> + </span> + </template> + </div> + <div class="item-time"> + <total-time :time="mergeRequest.totalTime"/> + </div> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js deleted file mode 100644 index d7c906c9d39..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable no-param-reassign */ -import Vue from 'vue'; -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; -import iconBranch from '../svg/icon_branch.svg'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.StageStagingComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - data() { - return { iconBranch }; - }, - components: { - userAvatarImage, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="build in items" class="stage-event-item item-build-component"> - <div class="item-details"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="build.author.avatarUrl"/> - <h5 class="item-title"> - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch">${iconBranch}</span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> - </h5> - <span> - <a :href="build.url" class="build-date">{{ build.date }}</a> - {{ s__('ByAuthor|by') }} - <a :href="build.author.webUrl" class="issue-author-link"> - {{ build.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="build.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue new file mode 100644 index 00000000000..2787b5ea47b --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -0,0 +1,55 @@ +<script> + import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; + import iconBranch from '../svg/icon_branch.svg'; + + export default { + props: { + items: Array, + stage: Object, + }, + components: { + userAvatarImage, + }, + computed: { + iconBranch() { + return iconBranch; + }, + }, + }; +</script> +<template> + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> + </div> + <ul class="stage-event-list"> + <li + v-for="(build, i) in items" + class="stage-event-item item-build-component" + :key="i"> + <div class="item-details"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="build.author.avatarUrl"/> + <h5 class="item-title"> + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> + <span class="icon-branch" v-html="iconBranch"></span> + <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="build-date">{{ build.date }}</a> + {{ s__('ByAuthor|by') }} + <a :href="build.author.webUrl" class="issue-author-link"> + {{ build.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"/> + </div> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js deleted file mode 100644 index 78cc97eea0b..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable no-param-reassign */ -import Vue from 'vue'; -import iconBuildStatus from '../svg/icon_build_status.svg'; -import iconBranch from '../svg/icon_branch.svg'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.StageTestComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - data() { - return { iconBuildStatus, iconBranch }; - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="build in items" class="stage-event-item item-build-component"> - <div class="item-details"> - <h5 class="item-title"> - <span class="icon-build-status">${iconBuildStatus}</span> - <a :href="build.url" class="item-build-name">{{ build.name }}</a> - · - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch">${iconBranch}</span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> - </h5> - <span> - <a :href="build.url" class="issue-date"> - {{ build.date }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="build.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue new file mode 100644 index 00000000000..9c3d39ce011 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -0,0 +1,54 @@ +<script> +import iconBuildStatus from '../svg/icon_build_status.svg'; +import iconBranch from '../svg/icon_branch.svg'; + +export default { + props: { + items: Array, + stage: Object, + }, + computed: { + iconBuildStatus() { + return iconBuildStatus; + }, + iconBranch() { + return iconBranch; + }, + }, +}; +</script> +<template> + <div> + <div class="events-description"> + {{ stage.description }} + <limit-warning :count="items.length" /> + </div> + <ul class="stage-event-list"> + <li + v-for="(build, i) in items" + :key="i" + class="stage-event-item item-build-component"> + <div class="item-details"> + <h5 class="item-title"> + <span class="icon-build-status" v-html="iconBuildStatus"></span> + <a :href="build.url" class="item-build-name">{{ build.name }}</a> + · + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> + <span class="icon-branch" v-html="iconBranch"></span> + <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="issue-date"> + {{ build.date }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"/> + </div> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js deleted file mode 100644 index d5e6167b2a8..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable no-param-reassign */ - -import Vue from 'vue'; - -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - -global.cycleAnalytics.TotalTimeComponent = Vue.extend({ - props: { - time: Object, - }, - template: ` - <span class="total-time"> - <template v-if="Object.keys(time).length"> - <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> - <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> - <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> - <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> - </template> - <template v-else> - -- - </template> - </span> - `, -}); diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue new file mode 100644 index 00000000000..9941b997b3f --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -0,0 +1,29 @@ +<script> + export default { + props: { + time: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + hasData() { + return Object.keys(this.time).length; + }, + }, + }; +</script> +<template> + <span class="total-time"> + <template v-if="hasData"> + <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> + <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> + <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> + <template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> + </template> + <template v-else> + -- + </template> + </span> +</template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 6583e471a48..8002b0b23c9 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -3,60 +3,63 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; import Translate from '../vue_shared/translate'; -import LimitWarningComponent from './components/limit_warning_component'; -import './components/stage_code_component'; -import './components/stage_issue_component'; -import './components/stage_plan_component'; -import './components/stage_production_component'; -import './components/stage_review_component'; -import './components/stage_staging_component'; -import './components/stage_test_component'; -import './components/total_time_component'; -import './cycle_analytics_service'; -import './cycle_analytics_store'; +import limitWarningComponent from './components/limit_warning_component.vue'; +import stageCodeComponent from './components/stage_code_component.vue'; +import stagePlanComponent from './components/stage_plan_component.vue'; +import stageComponent from './components/stage_component.vue'; +import stageReviewComponent from './components/stage_review_component.vue'; +import stageStagingComponent from './components/stage_staging_component.vue'; +import stageTestComponent from './components/stage_test_component.vue'; +import totalTime from './components/total_time_component.vue'; +import CycleAnalyticsService from './cycle_analytics_service'; +import CycleAnalyticsStore from './cycle_analytics_store'; Vue.use(Translate); $(() => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; - const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); - const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; - const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ - requestPath: cycleAnalyticsEl.dataset.requestPath, - }); gl.cycleAnalyticsApp = new Vue({ el: '#cycle-analytics', name: 'CycleAnalytics', - data: { - state: cycleAnalyticsStore.state, - isLoading: false, - isLoadingStage: false, - isEmptyStage: false, - hasError: false, - startDate: 30, - isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), + data() { + const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); + const cycleAnalyticsService = new CycleAnalyticsService({ + requestPath: cycleAnalyticsEl.dataset.requestPath, + }); + + return { + store: CycleAnalyticsStore, + state: CycleAnalyticsStore.state, + isLoading: false, + isLoadingStage: false, + isEmptyStage: false, + hasError: false, + startDate: 30, + isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), + service: cycleAnalyticsService, + }; }, computed: { currentStage() { - return cycleAnalyticsStore.currentActiveStage(); + return this.store.currentActiveStage(); }, }, components: { - 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, - 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, - 'stage-code-component': gl.cycleAnalytics.StageCodeComponent, - 'stage-test-component': gl.cycleAnalytics.StageTestComponent, - 'stage-review-component': gl.cycleAnalytics.StageReviewComponent, - 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, - 'stage-production-component': gl.cycleAnalytics.StageProductionComponent, + 'stage-issue-component': stageComponent, + 'stage-plan-component': stagePlanComponent, + 'stage-code-component': stageCodeComponent, + 'stage-test-component': stageTestComponent, + 'stage-review-component': stageReviewComponent, + 'stage-staging-component': stageStagingComponent, + 'stage-production-component': stageComponent, }, created() { this.fetchCycleAnalyticsData(); }, methods: { handleError() { - cycleAnalyticsStore.setErrorState(true); + this.store.setErrorState(true); return new Flash('There was an error while fetching cycle analytics data.'); }, initDropdown() { @@ -77,17 +80,17 @@ $(() => { this.isLoading = true; - cycleAnalyticsService + this.service .fetchCycleAnalyticsData(fetchOptions) - .done((response) => { - cycleAnalyticsStore.setCycleAnalyticsData(response); + .then(resp => resp.json()) + .then((response) => { + this.store.setCycleAnalyticsData(response); this.selectDefaultStage(); this.initDropdown(); + this.isLoading = false; }) - .error(() => { + .catch(() => { this.handleError(); - }) - .always(() => { this.isLoading = false; }); }, @@ -100,27 +103,27 @@ $(() => { if (this.currentStage === stage) return; if (!stage.isUserAllowed) { - cycleAnalyticsStore.setActiveStage(stage); + this.store.setActiveStage(stage); return; } this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([], stage); - cycleAnalyticsStore.setActiveStage(stage); + this.store.setStageEvents([], stage); + this.store.setActiveStage(stage); - cycleAnalyticsService + this.service .fetchStageData({ stage, startDate: this.startDate, }) - .done((response) => { + .then(resp => resp.json()) + .then((response) => { this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events, stage); + this.store.setStageEvents(response.events, stage); + this.isLoadingStage = false; }) - .error(() => { + .catch(() => { this.isEmptyStage = true; - }) - .always(() => { this.isLoadingStage = false; }); }, @@ -132,6 +135,6 @@ $(() => { }); // Register global components - Vue.component('limit-warning', LimitWarningComponent); - Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); + Vue.component('limit-warning', limitWarningComponent); + Vue.component('total-time', totalTime); }); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index 6504d7db2f2..f496c38208d 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -1,27 +1,16 @@ -/* eslint-disable no-param-reassign */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; +Vue.use(VueResource); -class CycleAnalyticsService { +export default class CycleAnalyticsService { constructor(options) { this.requestPath = options.requestPath; + this.cycleAnalytics = Vue.resource(this.requestPath); } - fetchCycleAnalyticsData(options) { - options = options || { startDate: 30 }; - - return $.ajax({ - url: this.requestPath, - method: 'GET', - dataType: 'json', - contentType: 'application/json', - data: { - cycle_analytics: { - start_date: options.startDate, - }, - }, - }); + fetchCycleAnalyticsData(options = { startDate: 30 }) { + return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } }); } fetchStageData(options) { @@ -30,12 +19,12 @@ class CycleAnalyticsService { startDate, } = options; - return $.get(`${this.requestPath}/events/${stage.name}.json`, { - cycle_analytics: { - start_date: startDate, + return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, { + params: { + cycle_analytics: { + start_date: startDate, + }, }, }); } } - -global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 991f8c1f6fd..8bf9ae17de0 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -4,9 +4,6 @@ import { __ } from '../locale'; import '../lib/utils/text_utility'; import DEFAULT_EVENT_OBJECTS from './default_event_objects'; -const global = window.gl || (window.gl = {}); -global.cycleAnalytics = global.cycleAnalytics || {}; - const EMPTY_STAGE_TEXTS = { issue: __('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.'), plan: __('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.'), @@ -17,7 +14,7 @@ const EMPTY_STAGE_TEXTS = { production: __('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.'), }; -global.cycleAnalytics.CycleAnalyticsStore = { +export default { state: { summary: '', stats: '', diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f3b537c83e2..bbaa4e4d91e 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -14,7 +14,6 @@ /* global NotificationsDropdown */ /* global GroupAvatar */ /* global LineHighlighter */ -/* global ProjectFork */ /* global BuildArtifacts */ /* global GroupsSelect */ /* global Search */ @@ -77,6 +76,7 @@ import initProjectVisibilitySelector from './project_visibility'; import GpgBadges from './gpg_badges'; import UserFeatureHelper from './helpers/user_feature_helper'; import initChangesDropdown from './init_changes_dropdown'; +import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; (function() { var Dispatcher; @@ -100,7 +100,7 @@ import initChangesDropdown from './init_changes_dropdown'; $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete); + const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); gfm.setup($(el), { emojis: true, members: enableGFM, @@ -351,7 +351,7 @@ import initChangesDropdown from './init_changes_dropdown'; if ($('.blob-viewer').length) new BlobViewer(); if ($('.project-show-activity').length) new gl.Activities(); $('#tree-slider').waitForImages(function() { - gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); break; case 'projects:edit': @@ -427,7 +427,7 @@ import initChangesDropdown from './init_changes_dropdown'; new NewCommitForm($('.js-create-dir-form')); new UserCallout({ setCalloutPerProject: true }); $('#tree-slider').waitForImages(function() { - gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); break; case 'projects:find_file:show': @@ -475,7 +475,9 @@ import initChangesDropdown from './init_changes_dropdown'; shortcut_handler = true; break; case 'projects:forks:new': - new ProjectFork(); + import(/* webpackChunkName: 'project_fork' */ './project_fork') + .then(fork => fork.default()) + .catch(() => {}); break; case 'projects:artifacts:browse': new ShortcutsNavigation(); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 975903159be..1cba65d17cd 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -2,6 +2,7 @@ /* global Dropzone */ import _ from 'underscore'; import './preview_markdown'; +import csrf from './lib/utils/csrf'; window.DropzoneInput = (function() { function DropzoneInput(form) { @@ -50,9 +51,7 @@ window.DropzoneInput = (function() { paramName: 'file', maxFilesize: maxFileSize, uploadMultiple: false, - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') - }, + headers: csrf.headers, previewContainer: false, processing: function() { return $('.div-dropzone-alert').alert('close'); @@ -260,9 +259,7 @@ window.DropzoneInput = (function() { dataType: 'json', processData: false, contentType: false, - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') - }, + headers: csrf.headers, beforeSend: function() { showSpinner(); return closeAlertMessage(); diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index f54d573db6e..ce5f6219a3e 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -6,7 +6,7 @@ import environmentTable from './environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; -import '../../lib/utils/common_utils'; +import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; import eventHub from '../event_hub'; import Poll from '../../lib/utils/poll'; import environmentsMixin from '../mixins/environments_mixin'; @@ -51,19 +51,19 @@ export default { computed: { scope() { - return gl.utils.getParameterByName('scope'); + return getParameterByName('scope'); }, canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + return convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + return convertPermissionToBoolean(this.canCreateDeployment); }, canCreateEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); + return convertPermissionToBoolean(this.canCreateEnvironment); }, }, @@ -72,8 +72,8 @@ export default { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.service = new EnvironmentsService(this.endpoint); @@ -126,15 +126,15 @@ export default { * @return {String} */ changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; }, fetchEnvironments() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.isLoading = true; @@ -163,7 +163,7 @@ export default { this.service.postAction(endpoint) .then(() => this.fetchEnvironments()) - .catch(() => new Flash('An error occured while making the request.')); + .catch(() => new Flash('An error occurred while making the request.')); } }, diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 925503a01c4..01e70c0bbb7 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -9,7 +9,7 @@ import tablePagination from '../../vue_shared/components/table_pagination.vue'; import Poll from '../../lib/utils/poll'; import eventHub from '../event_hub'; import environmentsMixin from '../mixins/environments_mixin'; -import '../../lib/utils/common_utils'; +import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; export default { components: { @@ -47,15 +47,15 @@ export default { computed: { scope() { - return gl.utils.getParameterByName('scope'); + return getParameterByName('scope'); }, canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + return convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + return convertPermissionToBoolean(this.canCreateDeployment); }, /** @@ -82,8 +82,8 @@ export default { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.service = new EnvironmentsService(this.endpoint); @@ -125,15 +125,15 @@ export default { * @param {Number} pageNumber desired page to go to. */ changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; }, fetchEnvironments() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.isLoading = true; @@ -158,7 +158,7 @@ export default { this.service.postAction(endpoint) .then(() => this.fetchEnvironments()) - .catch(() => new Flash('An error occured while making the request.')); + .catch(() => new Flash('An error occurred while making the request.')); } }, }, diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 038c149be2d..aff8227c38c 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -1,4 +1,4 @@ -import '~/lib/utils/common_utils'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; /** * Environments Store. * @@ -66,8 +66,8 @@ export default class EnvironmentsStore { } setPagination(pagination = {}) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + const paginationInformation = parseIntPagination(normalizedHeaders); this.state.paginationInformation = paginationInformation; return paginationInformation; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js deleted file mode 100644 index 800ca05cd11..00000000000 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ /dev/null @@ -1,61 +0,0 @@ -import Cookies from 'js-cookie'; -import _ from 'underscore'; -import { - getCookieName, - getSelector, - hidePopover, - setupDismissButton, - mouseenter, - mouseleave, -} from './feature_highlight_helper'; - -export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => { - const $selector = $(getSelector(id)); - const $parent = $selector.parent(); - const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); - const hideOnScroll = hidePopover.bind($selector); - const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout); - - $selector - // Setup popover - .data('content', $popoverContent.prop('outerHTML')) - .popover({ - html: true, - // Override the existing template to add custom CSS classes - template: ` - <div class="popover feature-highlight-popover" role="tooltip"> - <div class="arrow"></div> - <div class="popover-content"></div> - </div> - `, - }) - .on('mouseenter', mouseenter) - .on('mouseleave', debouncedMouseleave) - .on('inserted.bs.popover', setupDismissButton) - .on('show.bs.popover', () => { - window.addEventListener('scroll', hideOnScroll); - }) - .on('hide.bs.popover', () => { - window.removeEventListener('scroll', hideOnScroll); - }) - // Display feature highlight - .removeAttr('disabled'); -}; - -export const shouldHighlightFeature = (id) => { - const element = document.querySelector(getSelector(id)); - const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true'; - - return element && !previouslyDismissed; -}; - -export const highlightFeatures = (highlightOrder) => { - const featureId = highlightOrder.find(shouldHighlightFeature); - - if (featureId) { - setupFeatureHighlightPopover(featureId); - return true; - } - - return false; -}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js deleted file mode 100644 index 9f741355cd7..00000000000 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ /dev/null @@ -1,57 +0,0 @@ -import Cookies from 'js-cookie'; - -export const getCookieName = cookieId => `feature-highlighted-${cookieId}`; -export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; - -export const showPopover = function showPopover() { - if (this.hasClass('js-popover-show')) { - return false; - } - this.popover('show'); - this.addClass('disable-animation js-popover-show'); - - return true; -}; - -export const hidePopover = function hidePopover() { - if (!this.hasClass('js-popover-show')) { - return false; - } - this.popover('hide'); - this.removeClass('disable-animation js-popover-show'); - - return true; -}; - -export const dismiss = function dismiss(cookieId) { - Cookies.set(getCookieName(cookieId), true); - hidePopover.call(this); - this.hide(); -}; - -export const mouseleave = function mouseleave() { - if (!$('.popover:hover').length > 0) { - const $featureHighlight = $(this); - hidePopover.call($featureHighlight); - } -}; - -export const mouseenter = function mouseenter() { - const $featureHighlight = $(this); - - const showedPopover = showPopover.call($featureHighlight); - if (showedPopover) { - $('.popover') - .on('mouseleave', mouseleave.bind($featureHighlight)); - } -}; - -export const setupDismissButton = function setupDismissButton() { - const popoverId = this.getAttribute('aria-describedby'); - const cookieId = this.dataset.highlight; - const $popover = $(this); - const dismissWrapper = dismiss.bind($popover, cookieId); - - $(`#${popoverId} .dismiss-feature-highlight`) - .on('click', dismissWrapper); -}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js deleted file mode 100644 index fd48f2e87cc..00000000000 --- a/app/assets/javascripts/feature_highlight/feature_highlight_options.js +++ /dev/null @@ -1,12 +0,0 @@ -import { highlightFeatures } from './feature_highlight'; -import bp from '../breakpoints'; - -const highlightOrder = ['issue-boards']; - -export default function domContentLoaded(order) { - if (bp.getBreakpointSize() === 'lg') { - highlightFeatures(order); - } -} - -document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder)); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index d02e4cd5876..a00d29a845a 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -7,6 +7,8 @@ * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a */ +import Cookies from 'js-cookie'; + const LINE_NUMBER_CLASS = 'diff-line-num'; const UNFOLDABLE_LINE_CLASS = 'js-unfold'; const NO_COMMENT_CLASS = 'no-comment-btn'; @@ -27,9 +29,7 @@ export default { this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; } - if (typeof notes !== 'undefined' && !this.isParallelView) { - this.isParallelView = notes.isParallelView && notes.isParallelView(); - } + this.isParallelView = Cookies.get('diff_view') === 'parallel'; if (this.userCanCreateNote) { $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index f9bbbf0cbc1..ada14d2053c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -14,7 +14,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown { loadingTemplate: this.loadingTemplate, onError() { /* eslint-disable no-new */ - new Flash('An error occured fetching the dropdown data.'); + new Flash('An error occurred fetching the dropdown data.'); /* eslint-enable no-new */ }, }, diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 0bc4b6f22a9..b32d589481d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -17,7 +17,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { preprocessing, onError() { /* eslint-disable no-new */ - new Flash('An error occured fetching the dropdown data.'); + new Flash('An error occurred fetching the dropdown data.'); /* eslint-enable no-new */ }, }, diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 720fbc87ea0..ce8817b1b2e 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -26,7 +26,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { }, onError() { /* eslint-disable no-new */ - new Flash('An error occured fetching the dropdown data.'); + new Flash('An error occurred fetching the dropdown data.'); /* eslint-enable no-new */ }, }, diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 9178fec085a..a44dc279a6f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -36,7 +36,7 @@ class FilteredSearchManager { .catch((error) => { if (error.name === 'RecentSearchesServiceError') return undefined; // eslint-disable-next-line no-new - new window.Flash('An error occured while parsing recent searches'); + new window.Flash('An error occurred while parsing recent searches'); // Gracefully fail to empty array return []; }) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 157280d66e3..98837c3b2a0 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => { export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; export const getHideSubItemsInterval = () => { - if (!currentOpenMenu) return 0; + if (!currentOpenMenu || !mousePos.length) return 0; const currentMousePos = mousePos[mousePos.length - 1]; const prevMousePos = mousePos[0]; diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 36a04d4202f..d17a43b048a 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,6 +1,7 @@ <script> import tablePagination from '~/vue_shared/components/table_pagination.vue'; import eventHub from '../event_hub'; +import { getParameterByName } from '../../lib/utils/common_utils'; export default { props: { @@ -18,8 +19,8 @@ export default { }, methods: { change(page) { - const filterGroupsParam = gl.utils.getParameterByName('filter_groups'); - const sortParam = gl.utils.getParameterByName('sort'); + const filterGroupsParam = getParameterByName('filter_groups'); + const sortParam = getParameterByName('sort'); eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam); }, }, diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 439a931ddad..83b102764ba 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,5 +1,6 @@ import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; +import { getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { constructor({ form, filter, holder, filterEndpoint, pagePath }) { @@ -54,7 +55,7 @@ export default class GroupFilterableList extends FilterableList { e.preventDefault(); const queryData = {}; - const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href); + const sortParam = getParameterByName('sort', e.currentTarget.href); if (sortParam) { queryData.sort = sortParam; diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 00e1bd94c9c..9ad8e5c6052 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -8,6 +8,7 @@ import GroupItem from './components/group_item.vue'; import GroupsStore from './stores/groups_store'; import GroupsService from './services/groups_service'; import eventHub from './event_hub'; +import { getParameterByName } from '../lib/utils/common_utils'; document.addEventListener('DOMContentLoaded', () => { const el = document.getElementById('dashboard-group-app'); @@ -58,17 +59,17 @@ document.addEventListener('DOMContentLoaded', () => { this.isLoading = true; } - pageParam = gl.utils.getParameterByName('page'); + pageParam = getParameterByName('page'); if (pageParam) { page = pageParam; } - filterGroupsParam = gl.utils.getParameterByName('filter_groups'); + filterGroupsParam = getParameterByName('filter_groups'); if (filterGroupsParam) { filterGroups = filterGroupsParam; } - sortParam = gl.utils.getParameterByName('sort'); + sortParam = getParameterByName('sort'); if (sortParam) { sort = sortParam; } diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 6eab6083e8f..f59ec677603 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; export default class GroupsStore { constructor() { @@ -30,8 +31,8 @@ export default class GroupsStore { let paginationInfo; if (Object.keys(pagination).length) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + paginationInfo = parseIntPagination(normalizedHeaders); } else { paginationInfo = pagination; } diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 4d629bc6326..90ca70289ab 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -4,6 +4,7 @@ prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, promise/catch-or-return */ import Api from './api'; +import { normalizeCRLFHeaders } from './lib/utils/common_utils'; var slice = [].slice; @@ -30,7 +31,7 @@ window.GroupsSelect = (function() { $.ajax(params).then((data, status, xhr) => { const results = data || []; - const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); const currentPage = parseInt(headers['X-PAGE'], 10) || 0; const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; const more = currentPage < totalPages; diff --git a/app/assets/javascripts/help/help.js b/app/assets/javascripts/help/help.js new file mode 100644 index 00000000000..4a22ebf187d --- /dev/null +++ b/app/assets/javascripts/help/help.js @@ -0,0 +1,6 @@ +// We will render the icons list here +if ($('#user-content-gitlab-icons').length > 0) { + const $iconsHeader = $('#user-content-gitlab-icons'); + const $iconsList = $('<div id="iconsList">ICONS</div>'); + $($iconsList).insertAfter($iconsHeader.parent()); +} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b8bebe1894f..423a25fbdfa 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,437 +1,438 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */ -(function() { - (function(w) { - var base; - const faviconEl = document.getElementById('favicon'); - const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; - w.gl || (w.gl = {}); - (base = w.gl).utils || (base.utils = {}); - w.gl.utils.isInGroupsPage = function() { - return gl.utils.getPagePath() === 'groups'; - }; - w.gl.utils.isInProjectPage = function() { - return gl.utils.getPagePath() === 'projects'; - }; - w.gl.utils.getProjectSlug = function() { - if (this.isInProjectPage()) { - return $('body').data('project'); - } else { - return null; - } - }; - w.gl.utils.getGroupSlug = function() { - if (this.isInGroupsPage()) { - return $('body').data('group'); - } else { - return null; - } - }; - - w.gl.utils.isInIssuePage = () => { - const page = gl.utils.getPagePath(1); - const action = gl.utils.getPagePath(2); - - return page === 'issues' && action === 'show'; - }; - w.gl.utils.ajaxGet = function(url) { - return $.ajax({ - type: "GET", - url: url, - dataType: "script" - }); - }; - - w.gl.utils.ajaxPost = function(url, data) { - return $.ajax({ - type: 'POST', - url: url, - data: data, - }); - }; - - w.gl.utils.extractLast = function(term) { - return this.split(term).pop(); - }; - - w.gl.utils.rstrip = function rstrip(val) { - if (val) { - return val.replace(/\s+$/, ''); +export const getPagePath = (index = 0) => $('body').data('page').split(':')[index]; + +export const isInGroupsPage = () => getPagePath() === 'groups'; + +export const isInProjectPage = () => getPagePath() === 'projects'; + +export const getProjectSlug = () => { + if (isInProjectPage()) { + return $('body').data('project'); + } + return null; +}; + +export const getGroupSlug = () => { + if (isInGroupsPage()) { + return $('body').data('group'); + } + return null; +}; + +export const isInIssuePage = () => { + const page = getPagePath(1); + const action = getPagePath(2); + + return page === 'issues' && action === 'show'; +}; + +export const ajaxGet = url => $.ajax({ + type: 'GET', + url, + dataType: 'script', +}); + +export const ajaxPost = (url, data) => $.ajax({ + type: 'POST', + url, + data, +}); + +export const rstrip = (val) => { + if (val) { + return val.replace(/\s+$/, ''); + } + return val; +}; + +export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); + +export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { + const field = $(fieldSelector); + const closestSubmit = field.closest('form').find(buttonSelector); + if (rstrip(field.val()) === '') { + closestSubmit.disable(); + } + // eslint-disable-next-line func-names + return field.on(eventName, function () { + if (rstrip($(this).val()) === '') { + return closestSubmit.disable(); + } + return closestSubmit.enable(); + }); +}; + +// automatically adjust scroll position for hash urls taking the height of the navbar into account +// https://github.com/twitter/bootstrap/issues/1768 +export const handleLocationHash = () => { + let hash = window.gl.utils.getLocationHash(); + if (!hash) return; + + // This is required to handle non-unicode characters in hash + hash = decodeURIComponent(hash); + + const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`); + const fixedTabs = document.querySelector('.js-tabs-affix'); + const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); + const fixedNav = document.querySelector('.navbar-gitlab'); + + let adjustment = 0; + if (fixedNav) adjustment -= fixedNav.offsetHeight; + + if (target && target.scrollIntoView) { + target.scrollIntoView(true); + } + + if (fixedTabs) { + adjustment -= fixedTabs.offsetHeight; + } + + if (fixedDiffStats) { + adjustment -= fixedDiffStats.offsetHeight; + } + + window.scrollBy(0, adjustment); +}; + +// Check if element scrolled into viewport from above or below +// Courtesy http://stackoverflow.com/a/7557433/414749 +export const isInViewport = (el) => { + const rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); +}; + +export const parseUrl = (url) => { + const parser = document.createElement('a'); + parser.href = url; + return parser; +}; + +export const parseUrlPathname = (url) => { + const parsedUrl = parseUrl(url); + // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 + // We have to make sure we always have an absolute path. + return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`; +}; + +// We can trust that each param has one & since values containing & will be encoded +// Remove the first character of search as it is always ? +export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => { + const split = param.split('='); + return [decodeURI(split[0]), split[1]].join('='); +}); + +export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; + +// Identify following special clicks +// 1) Cmd + Click on Mac (e.metaKey) +// 2) Ctrl + Click on PC (e.ctrlKey) +// 3) Middle-click or Mouse Wheel Click (e.which is 2) +export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; + +export const scrollToElement = ($el) => { + const top = $el.offset().top; + const mrTabsHeight = $('.merge-request-tabs').height() || 0; + const headerHeight = $('.navbar-gitlab').height() || 0; + + return $('body, html').animate({ + scrollTop: top - mrTabsHeight - headerHeight, + }, 200); +}; + +/** + this will take in the `name` of the param you want to parse in the url + if the name does not exist this function will return `null` + otherwise it will return the value of the param key provided +*/ +export const getParameterByName = (name, urlToParse) => { + const url = urlToParse || window.location.href; + const parsedName = name.replace(/[[\]]/g, '\\$&'); + const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +}; + +export const getSelectedFragment = () => { + const selection = window.getSelection(); + if (selection.rangeCount === 0) return null; + const documentFragment = document.createDocumentFragment(); + for (let i = 0; i < selection.rangeCount; i += 1) { + documentFragment.appendChild(selection.getRangeAt(i).cloneContents()); + } + if (documentFragment.textContent.length === 0) return null; + + return documentFragment; +}; + +// TODO: Update this name, there is a gl.text.insertText function. +export const insertText = (target, text) => { + // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas + const selectionStart = target.selectionStart; + const selectionEnd = target.selectionEnd; + const value = target.value; + + const textBefore = value.substring(0, selectionStart); + const textAfter = value.substring(selectionEnd, value.length); + + const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; + const newText = textBefore + insertedText + textAfter; + + // eslint-disable-next-line no-param-reassign + target.value = newText; + // eslint-disable-next-line no-param-reassign + target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; + + // Trigger autosave + $(target).trigger('input'); + + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + target.dispatchEvent(event); +}; + +export const nodeMatchesSelector = (node, selector) => { + const matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector; + + if (matches) { + return matches.call(node, selector); + } + + // IE11 doesn't support `node.matches(selector)` + + let parentNode = node.parentNode; + if (!parentNode) { + parentNode = document.createElement('div'); + // eslint-disable-next-line no-param-reassign + node = node.cloneNode(true); + parentNode.appendChild(node); + } + + const matchingNodes = parentNode.querySelectorAll(selector); + return Array.prototype.indexOf.call(matchingNodes, node) !== -1; +}; + +/** + this will take in the headers from an API response and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys +*/ +export const normalizeHeaders = (headers) => { + const upperCaseHeaders = {}; + + Object.keys(headers).forEach((e) => { + upperCaseHeaders[e.toUpperCase()] = headers[e]; + }); + + return upperCaseHeaders; +}; + +/** + this will take in the getAllResponseHeaders result and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys +*/ +export const normalizeCRLFHeaders = (headers) => { + const headersObject = {}; + const headersArray = headers.split('\n'); + + headersArray.forEach((header) => { + const keyValue = header.split(': '); + headersObject[keyValue[0]] = keyValue[1]; + }); + + return normalizeHeaders(headersObject); +}; + +/** + * Parses pagination object string values into numbers. + * + * @param {Object} paginationInformation + * @returns {Object} + */ +export const parseIntPagination = paginationInformation => ({ + perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), + page: parseInt(paginationInformation['X-PAGE'], 10), + total: parseInt(paginationInformation['X-TOTAL'], 10), + totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), + nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), + previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), +}); + +/** + * Updates the search parameter of a URL given the parameter and value provided. + * + * If no search params are present we'll add it. + * If param for page is already present, we'll update it + * If there are params but not for the given one, we'll add it at the end. + * Returns the new search parameters. + * + * @param {String} param + * @param {Number|String|Undefined|Null} value + * @return {String} + */ +export const setParamInURL = (param, value) => { + let search; + const locationSearch = window.location.search; + + if (locationSearch.length) { + const parameters = locationSearch.substring(1, locationSearch.length) + .split('&') + .reduce((acc, element) => { + const val = element.split('='); + // eslint-disable-next-line no-param-reassign + acc[val[0]] = decodeURIComponent(val[1]); + return acc; + }, {}); + + parameters[param] = value; + + const toString = Object.keys(parameters) + .map(val => `${val}=${encodeURIComponent(parameters[val])}`) + .join('&'); + + search = `?${toString}`; + } else { + search = `?${param}=${value}`; + } + + return search; +}; + +/** + * Converts permission provided as strings to booleans. + * + * @param {String} string + * @returns {Boolean} + */ +export const convertPermissionToBoolean = permission => permission === 'true'; + +/** + * Back Off exponential algorithm + * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> + * + * @param {Function<next, stop>} fn function to be called + * @param {Number} timeout + * @return {Promise<Any, Error>} + * @example + * ``` + * backOff(function (next, stop) { + * // Let's perform this function repeatedly for 60s or for the timeout provided. + * + * ourFunction() + * .then(function (result) { + * // continue if result is not what we need + * next(); + * + * // when result is what we need let's stop with the repetions and jump out of the cycle + * stop(result); + * }) + * .catch(function (error) { + * // if there is an error, we need to stop this with an error. + * stop(error); + * }) + * }, 60000) + * .then(function (result) {}) + * .catch(function (error) { + * // deal with errors passed to stop() + * }) + * ``` + */ +export const backOff = (fn, timeout = 60000) => { + const maxInterval = 32000; + let nextInterval = 2000; + let timeElapsed = 0; + + return new Promise((resolve, reject) => { + const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); + + const next = () => { + if (timeElapsed < timeout) { + setTimeout(() => fn(next, stop), nextInterval); + timeElapsed += nextInterval; + nextInterval = Math.min(nextInterval + nextInterval, maxInterval); } else { - return val; - } - }; - - gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { - return $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); - }; - - w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { - event_name = event_name || 'input'; - var closest_submit, field, that; - that = this; - field = $(field_selector); - closest_submit = field.closest('form').find(button_selector); - if (this.rstrip(field.val()) === "") { - closest_submit.disable(); + reject(new Error('BACKOFF_TIMEOUT')); } - return field.on(event_name, function() { - if (that.rstrip($(this).val()) === "") { - return closest_submit.disable(); - } else { - return closest_submit.enable(); - } - }); }; - // automatically adjust scroll position for hash urls taking the height of the navbar into account - // https://github.com/twitter/bootstrap/issues/1768 - w.gl.utils.handleLocationHash = function() { - var hash = w.gl.utils.getLocationHash(); - if (!hash) return; - - // This is required to handle non-unicode characters in hash - hash = decodeURIComponent(hash); - - const fixedTabs = document.querySelector('.js-tabs-affix'); - const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); - const fixedNav = document.querySelector('.navbar-gitlab'); - - var adjustment = 0; - if (fixedNav) adjustment -= fixedNav.offsetHeight; - - // scroll to user-generated markdown anchor if we cannot find a match - if (document.getElementById(hash) === null) { - var target = document.getElementById('user-content-' + hash); - if (target && target.scrollIntoView) { - target.scrollIntoView(true); - window.scrollBy(0, adjustment); - } + fn(next, stop); + }); +}; + +export const setFavicon = (faviconPath) => { + const faviconEl = document.getElementById('favicon'); + if (faviconEl && faviconPath) { + faviconEl.setAttribute('href', faviconPath); + } +}; + +export const resetFavicon = () => { + const faviconEl = document.getElementById('favicon'); + const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; + if (faviconEl) { + faviconEl.setAttribute('href', originalFavicon); + } +}; + +export const setCiStatusFavicon = (pageUrl) => { + $.ajax({ + url: pageUrl, + dataType: 'json', + success: (data) => { + if (data && data.favicon) { + setFavicon(data.favicon); } else { - // only adjust for fixedTabs when not targeting user-generated content - if (fixedTabs) { - adjustment -= fixedTabs.offsetHeight; - } - - if (fixedDiffStats) { - adjustment -= fixedDiffStats.offsetHeight; - } - - window.scrollBy(0, adjustment); + resetFavicon(); } - }; - - // Check if element scrolled into viewport from above or below - // Courtesy http://stackoverflow.com/a/7557433/414749 - w.gl.utils.isInViewport = function(el) { - var rect = el.getBoundingClientRect(); - - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth - ); - }; - - gl.utils.getPagePath = function(index) { - index = index || 0; - return $('body').data('page').split(':')[index]; - }; - - gl.utils.parseUrl = function (url) { - var parser = document.createElement('a'); - parser.href = url; - return parser; - }; - - gl.utils.parseUrlPathname = function (url) { - var parsedUrl = gl.utils.parseUrl(url); - // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 - // We have to make sure we always have an absolute path. - return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; - }; - - gl.utils.getUrlParamsArray = function () { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - return window.location.search.slice(1).split('&').map((param) => { - const split = param.split('='); - return [decodeURI(split[0]), split[1]].join('='); - }); - }; - - gl.utils.isMetaKey = function(e) { - return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; - }; - - gl.utils.isMetaClick = function(e) { - // Identify following special clicks - // 1) Cmd + Click on Mac (e.metaKey) - // 2) Ctrl + Click on PC (e.ctrlKey) - // 3) Middle-click or Mouse Wheel Click (e.which is 2) - return e.metaKey || e.ctrlKey || e.which === 2; - }; - - gl.utils.scrollToElement = function($el) { - const top = $el.offset().top; - const mrTabsHeight = $('.merge-request-tabs').height() || 0; - const headerHeight = $('.navbar-gitlab').height() || 0; - - return $('body, html').animate({ - scrollTop: top - mrTabsHeight - headerHeight, - }, 200); - }; - - /** - this will take in the `name` of the param you want to parse in the url - if the name does not exist this function will return `null` - otherwise it will return the value of the param key provided - */ - w.gl.utils.getParameterByName = (name, parseUrl) => { - const url = parseUrl || window.location.href; - name = name.replace(/[[\]]/g, '\\$&'); - const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); - const results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }; - - w.gl.utils.getSelectedFragment = () => { - const selection = window.getSelection(); - if (selection.rangeCount === 0) return null; - const documentFragment = document.createDocumentFragment(); - for (let i = 0; i < selection.rangeCount; i += 1) { - documentFragment.appendChild(selection.getRangeAt(i).cloneContents()); - } - if (documentFragment.textContent.length === 0) return null; - - return documentFragment; - }; - - w.gl.utils.insertText = (target, text) => { - // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas - - const selectionStart = target.selectionStart; - const selectionEnd = target.selectionEnd; - const value = target.value; - - const textBefore = value.substring(0, selectionStart); - const textAfter = value.substring(selectionEnd, value.length); - - const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; - const newText = textBefore + insertedText + textAfter; - - target.value = newText; - target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; - - // Trigger autosave - $(target).trigger('input'); - - // Trigger autosize - var event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - target.dispatchEvent(event); - }; - - w.gl.utils.nodeMatchesSelector = (node, selector) => { - const matches = Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector; - - if (matches) { - return matches.call(node, selector); - } - - // IE11 doesn't support `node.matches(selector)` - - let parentNode = node.parentNode; - if (!parentNode) { - parentNode = document.createElement('div'); - node = node.cloneNode(true); - parentNode.appendChild(node); - } - - const matchingNodes = parentNode.querySelectorAll(selector); - return Array.prototype.indexOf.call(matchingNodes, node) !== -1; - }; - - /** - this will take in the headers from an API response and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys - */ - w.gl.utils.normalizeHeaders = (headers) => { - const upperCaseHeaders = {}; - - Object.keys(headers).forEach((e) => { - upperCaseHeaders[e.toUpperCase()] = headers[e]; - }); - - return upperCaseHeaders; - }; - - /** - this will take in the getAllResponseHeaders result and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys - */ - w.gl.utils.normalizeCRLFHeaders = (headers) => { - const headersObject = {}; - const headersArray = headers.split('\n'); - - headersArray.forEach((header) => { - const keyValue = header.split(': '); - headersObject[keyValue[0]] = keyValue[1]; - }); - - return w.gl.utils.normalizeHeaders(headersObject); - }; - - /** - * Parses pagination object string values into numbers. - * - * @param {Object} paginationInformation - * @returns {Object} - */ - w.gl.utils.parseIntPagination = paginationInformation => ({ - perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), - page: parseInt(paginationInformation['X-PAGE'], 10), - total: parseInt(paginationInformation['X-TOTAL'], 10), - totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), - nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), - previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), - }); - - /** - * Updates the search parameter of a URL given the parameter and value provided. - * - * If no search params are present we'll add it. - * If param for page is already present, we'll update it - * If there are params but not for the given one, we'll add it at the end. - * Returns the new search parameters. - * - * @param {String} param - * @param {Number|String|Undefined|Null} value - * @return {String} - */ - w.gl.utils.setParamInURL = (param, value) => { - let search; - const locationSearch = window.location.search; - - if (locationSearch.length) { - const parameters = locationSearch.substring(1, locationSearch.length) - .split('&') - .reduce((acc, element) => { - const val = element.split('='); - acc[val[0]] = decodeURIComponent(val[1]); - return acc; - }, {}); - - parameters[param] = value; - - const toString = Object.keys(parameters) - .map(val => `${val}=${encodeURIComponent(parameters[val])}`) - .join('&'); - - search = `?${toString}`; - } else { - search = `?${param}=${value}`; - } - - return search; - }; - - /** - * Converts permission provided as strings to booleans. - * - * @param {String} string - * @returns {Boolean} - */ - w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; - - /** - * Back Off exponential algorithm - * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> - * - * @param {Function<next, stop>} fn function to be called - * @param {Number} timeout - * @return {Promise<Any, Error>} - * @example - * ``` - * backOff(function (next, stop) { - * // Let's perform this function repeatedly for 60s or for the timeout provided. - * - * ourFunction() - * .then(function (result) { - * // continue if result is not what we need - * next(); - * - * // when result is what we need let's stop with the repetions and jump out of the cycle - * stop(result); - * }) - * .catch(function (error) { - * // if there is an error, we need to stop this with an error. - * stop(error); - * }) - * }, 60000) - * .then(function (result) {}) - * .catch(function (error) { - * // deal with errors passed to stop() - * }) - * ``` - */ - w.gl.utils.backOff = (fn, timeout = 60000) => { - const maxInterval = 32000; - let nextInterval = 2000; - let timeElapsed = 0; - - return new Promise((resolve, reject) => { - const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); - - const next = () => { - if (timeElapsed < timeout) { - setTimeout(() => fn(next, stop), nextInterval); - timeElapsed += nextInterval; - nextInterval = Math.min(nextInterval + nextInterval, maxInterval); - } else { - reject(new Error('BACKOFF_TIMEOUT')); - } - }; - - fn(next, stop); - }); - }; - - w.gl.utils.setFavicon = (faviconPath) => { - if (faviconEl && faviconPath) { - faviconEl.setAttribute('href', faviconPath); - } - }; - - w.gl.utils.resetFavicon = () => { - if (faviconEl) { - faviconEl.setAttribute('href', originalFavicon); - } - }; - - w.gl.utils.setCiStatusFavicon = (pageUrl) => { - $.ajax({ - url: pageUrl, - dataType: 'json', - success: function(data) { - if (data && data.favicon) { - gl.utils.setFavicon(data.favicon); - } else { - gl.utils.resetFavicon(); - } - }, - error: function() { - gl.utils.resetFavicon(); - } - }); - }; - })(window); -}).call(window); + }, + error: () => { + resetFavicon(); + }, + }); +}; + +export const spriteIcon = icon => `<svg><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`; + +export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; + +window.gl = window.gl || {}; +window.gl.utils = { + ...(window.gl.utils || {}), + getPagePath, + isInGroupsPage, + isInProjectPage, + getProjectSlug, + getGroupSlug, + isInIssuePage, + ajaxGet, + ajaxPost, + rstrip, + updateTooltipTitle, + disableButtonIfEmptyField, + handleLocationHash, + isInViewport, + parseUrl, + parseUrlPathname, + getUrlParamsArray, + isMetaKey, + isMetaClick, + scrollToElement, + getParameterByName, + getSelectedFragment, + insertText, + nodeMatchesSelector, + spriteIcon, + imagePath, +}; diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js new file mode 100644 index 00000000000..ae41cc5e8a8 --- /dev/null +++ b/app/assets/javascripts/lib/utils/csrf.js @@ -0,0 +1,56 @@ +/* +This module provides easy access to the CSRF token and caches +it for re-use. It also exposes some values commonly used in relation +to the CSRF token (header key and headers object). + +If you need to refresh the csrfToken for some reason, just call `init` and +then use the accessors as you would normally. + +If you need to compose a headers object, use the spread operator: + +``` + headers: { + ...csrf.headers, + someOtherHeader: '12345', + } +``` + */ + +const csrf = { + init() { + const tokenEl = document.querySelector('meta[name=csrf-token]'); + + if (tokenEl !== null) { + this.csrfToken = tokenEl.getAttribute('content'); + } else { + this.csrfToken = null; + } + }, + + get token() { + return this.csrfToken; + }, + + get headerKey() { + return 'X-CSRF-Token'; + }, + + get headers() { + if (this.csrfToken !== null) { + return { + [this.headerKey]: this.token, + }; + } + return {}; + }, +}; + +csrf.init(); + +// use our cached token for any $.rails-generated AJAX requests +if ($.rails) { + $.rails.csrfToken = () => csrf.token; +} + +export default csrf; + diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 97666e13ebe..1485e900945 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -1,4 +1,5 @@ import httpStatusCodes from './http_status'; +import { normalizeHeaders } from './common_utils'; /** * Polling utility for handling realtime updates. @@ -57,7 +58,7 @@ export default class Poll { } checkConditions(response) { - const headers = gl.utils.normalizeHeaders(response.headers); + const headers = normalizeHeaders(response.headers); const pollInterval = parseInt(headers[this.intervalHeader], 10); if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js index 227bf65b560..b1ffd797f7e 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ b/app/assets/javascripts/lib/utils/pretty_time.js @@ -1,68 +1,61 @@ import _ from 'underscore'; -(() => { - /* - * TODO: Make these methods more configurable (e.g. stringifyTime condensed or - * non-condensed, abbreviateTimelengths) - * */ - - const utils = window.gl.utils = gl.utils || {}; - const prettyTime = utils.prettyTime = { - /* - * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } - * Seconds can be negative or positive, zero or non-zero. Can be configured for any day - * or week length. - */ - parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { - const DAYS_PER_WEEK = daysPerWeek; - const HOURS_PER_DAY = hoursPerDay; - const MINUTES_PER_HOUR = 60; - const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; - const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; - - const timePeriodConstraints = { - weeks: MINUTES_PER_WEEK, - days: MINUTES_PER_DAY, - hours: MINUTES_PER_HOUR, - minutes: 1, - }; +/* + * TODO: Make these methods more configurable (e.g. stringifyTime condensed or + * non-condensed, abbreviateTimelengths) + * */ + +/* + * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } + * Seconds can be negative or positive, zero or non-zero. Can be configured for any day + * or week length. +*/ + +export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { + const DAYS_PER_WEEK = daysPerWeek; + const HOURS_PER_DAY = hoursPerDay; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; - let unorderedMinutes = prettyTime.secondsToMinutes(seconds); + let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); - return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { - const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); - unorderedMinutes -= (periodCount * minutesPerPeriod); + unorderedMinutes -= (periodCount * minutesPerPeriod); - return periodCount; - }); - }, + return periodCount; + }); +} - /* - * Accepts a timeObject and returns a condensed string representation of it - * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. - */ +/* +* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it +* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. +*/ - stringifyTime(timeObject) { - const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { - const isNonZero = !!unitValue; - return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; - }, '').trim(); - return reducedTime.length ? reducedTime : '0m'; - }, +export function stringifyTime(timeObject) { + const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, '').trim(); + return reducedTime.length ? reducedTime : '0m'; +} - /* - * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns - * the first non-zero unit/value pair. - */ +/* +* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns +* the first non-zero unit/value pair. +*/ - abbreviateTime(timeStr) { - return timeStr.split(' ') - .filter(unitStr => unitStr.charAt(0) !== '0')[0]; - }, +export function abbreviateTime(timeStr) { + return timeStr.split(' ') + .filter(unitStr => unitStr.charAt(0) !== '0')[0]; +} - secondsToMinutes(seconds) { - return Math.abs(seconds / 60); - }, - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 283c0ec0410..64db42701ce 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,14 +1,34 @@ -export const isSticky = (el, scrollY, stickyTop) => { +export const createPlaceholder = () => { + const placeholder = document.createElement('div'); + placeholder.classList.add('sticky-placeholder'); + + return placeholder; +}; + +export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { const top = Math.floor(el.offsetTop - scrollY); - if (top <= stickyTop) { + if (top <= stickyTop && !el.classList.contains('is-stuck')) { + const placeholder = insertPlaceholder ? createPlaceholder() : null; + const heightBefore = el.offsetHeight; + el.classList.add('is-stuck'); - } else { + + if (insertPlaceholder) { + el.parentNode.insertBefore(placeholder, el.nextElementSibling); + + placeholder.style.height = `${heightBefore - el.offsetHeight}px`; + } + } else if (top > stickyTop && el.classList.contains('is-stuck')) { el.classList.remove('is-stuck'); + + if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) { + el.nextElementSibling.remove(); + } } }; -export default (el) => { +export default (el, insertPlaceholder = true) => { if (!el) return; const computedStyle = window.getComputedStyle(el); @@ -17,7 +37,7 @@ export default (el) => { const stickyTop = parseInt(computedStyle.top, 10); - document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), { + document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), { passive: true, }); }; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 7400c22543f..a16d00b5cef 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -28,148 +28,149 @@ // </div> // </div> // -(function() { - this.LineHighlighter = (function() { - // CSS class applied to highlighted lines - LineHighlighter.prototype.highlightClass = 'hll'; - - // Internal copy of location.hash so we're not dependent on `location` in tests - LineHighlighter.prototype._hash = ''; - - function LineHighlighter(hash) { - if (hash == null) { - // Initialize a LineHighlighter object - // - // hash - String URL hash for dependency injection in tests - hash = location.hash; - } - this.setHash = this.setHash.bind(this); - this.highlightLine = this.highlightLine.bind(this); - this.clickHandler = this.clickHandler.bind(this); - this.highlightHash = this.highlightHash.bind(this); - this._hash = hash; - this.bindEvents(); - this.highlightHash(); - } - LineHighlighter.prototype.bindEvents = function() { - const $fileHolder = $('.file-holder'); - $fileHolder.on('click', 'a[data-line-number]', this.clickHandler); - $fileHolder.on('highlight:line', this.highlightHash); - }; - - LineHighlighter.prototype.highlightHash = function() { - var range; - if (this._hash !== '') { - range = this.hashToRange(this._hash); - if (range[0]) { - this.highlightRange(range); - $.scrollTo("#L" + range[0], { - // Scroll to the first highlighted line on initial load - // Offset -50 for the sticky top bar, and another -100 for some context - offset: -150 - }); - } - } - }; - - LineHighlighter.prototype.clickHandler = function(event) { - var current, lineNumber, range; - event.preventDefault(); - this.clearHighlight(); - lineNumber = $(event.target).closest('a').data('line-number'); - current = this.hashToRange(this._hash); - if (!(current[0] && event.shiftKey)) { - // If there's no current selection, or there is but Shift wasn't held, - // treat this like a single-line selection. - this.setHash(lineNumber); - return this.highlightLine(lineNumber); - } else if (event.shiftKey) { - if (lineNumber < current[0]) { - range = [lineNumber, current[0]]; - } else { - range = [current[0], lineNumber]; - } - this.setHash(range[0], range[1]); - return this.highlightRange(range); - } - }; - - LineHighlighter.prototype.clearHighlight = function() { - return $("." + this.highlightClass).removeClass(this.highlightClass); - // Unhighlight previously highlighted lines - }; - - // Convert a URL hash String into line numbers - // - // hash - Hash String - // - // Examples: - // - // hashToRange('#L5') # => [5, null] - // hashToRange('#L5-15') # => [5, 15] - // hashToRange('#foo') # => [null, null] - // - // Returns an Array - LineHighlighter.prototype.hashToRange = function(hash) { - var first, last, matches; - // ?L(\d+)(?:-(\d+))?$/) - matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); - if (matches && matches.length) { - first = parseInt(matches[1], 10); - last = matches[2] ? parseInt(matches[2], 10) : null; - return [first, last]; - } else { - return [null, null]; - } - }; - - // Highlight a single line - // - // lineNumber - Line number to highlight - LineHighlighter.prototype.highlightLine = function(lineNumber) { - return $("#LC" + lineNumber).addClass(this.highlightClass); - }; - - // Highlight all lines within a range - // - // range - Array containing the starting and ending line numbers - LineHighlighter.prototype.highlightRange = function(range) { - var i, lineNumber, ref, ref1, results; - if (range[1]) { - results = []; - for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) { - results.push(this.highlightLine(lineNumber)); - } - return results; - } else { - return this.highlightLine(range[0]); - } - }; +const LineHighlighter = function(options = {}) { + options.highlightLineClass = options.highlightLineClass || 'hll'; + options.fileHolderSelector = options.fileHolderSelector || '.file-holder'; + options.scrollFileHolder = options.scrollFileHolder || false; + options.hash = options.hash || location.hash; - // Set the URL hash string - LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { - var hash; - if (lastLineNumber) { - hash = "#L" + firstLineNumber + "-" + lastLineNumber; + this.options = options; + this._hash = options.hash; + this.highlightLineClass = options.highlightLineClass; + this.setHash = this.setHash.bind(this); + this.highlightLine = this.highlightLine.bind(this); + this.clickHandler = this.clickHandler.bind(this); + this.highlightHash = this.highlightHash.bind(this); + + this.bindEvents(); + this.highlightHash(); +}; + +LineHighlighter.prototype.bindEvents = function() { + const $fileHolder = $(this.options.fileHolderSelector); + + $fileHolder.on('click', 'a[data-line-number]', this.clickHandler); + $fileHolder.on('highlight:line', this.highlightHash); +}; + +LineHighlighter.prototype.highlightHash = function() { + var range; + + if (this._hash !== '') { + range = this.hashToRange(this._hash); + + if (range[0]) { + this.highlightRange(range); + const lineSelector = `#L${range[0]}`; + const scrollOptions = { + // Scroll to the first highlighted line on initial load + // Offset -50 for the sticky top bar, and another -100 for some context + offset: -150 + }; + if (this.options.scrollFileHolder) { + $(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions); } else { - hash = "#L" + firstLineNumber; + $.scrollTo(lineSelector, scrollOptions); } - this._hash = hash; - return this.__setLocationHash__(hash); - }; - - // Make the actual hash change in the browser - // - // This method is stubbed in tests. - LineHighlighter.prototype.__setLocationHash__ = function(value) { - return history.pushState({ - url: value - // We're using pushState instead of assigning location.hash directly to - // prevent the page from scrolling on the hashchange event - }, document.title, value); - }; - - return LineHighlighter; - })(); -}).call(window); + } + } +}; + +LineHighlighter.prototype.clickHandler = function(event) { + var current, lineNumber, range; + event.preventDefault(); + this.clearHighlight(); + lineNumber = $(event.target).closest('a').data('line-number'); + current = this.hashToRange(this._hash); + if (!(current[0] && event.shiftKey)) { + // If there's no current selection, or there is but Shift wasn't held, + // treat this like a single-line selection. + this.setHash(lineNumber); + return this.highlightLine(lineNumber); + } else if (event.shiftKey) { + if (lineNumber < current[0]) { + range = [lineNumber, current[0]]; + } else { + range = [current[0], lineNumber]; + } + this.setHash(range[0], range[1]); + return this.highlightRange(range); + } +}; + +LineHighlighter.prototype.clearHighlight = function() { + return $("." + this.highlightLineClass).removeClass(this.highlightLineClass); +}; + +// Convert a URL hash String into line numbers +// +// hash - Hash String +// +// Examples: +// +// hashToRange('#L5') # => [5, null] +// hashToRange('#L5-15') # => [5, 15] +// hashToRange('#foo') # => [null, null] +// +// Returns an Array +LineHighlighter.prototype.hashToRange = function(hash) { + var first, last, matches; + // ?L(\d+)(?:-(\d+))?$/) + matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); + if (matches && matches.length) { + first = parseInt(matches[1], 10); + last = matches[2] ? parseInt(matches[2], 10) : null; + return [first, last]; + } else { + return [null, null]; + } +}; + +// Highlight a single line +// +// lineNumber - Line number to highlight +LineHighlighter.prototype.highlightLine = function(lineNumber) { + return $("#LC" + lineNumber).addClass(this.highlightLineClass); +}; + +// Highlight all lines within a range +// +// range - Array containing the starting and ending line numbers +LineHighlighter.prototype.highlightRange = function(range) { + var i, lineNumber, ref, ref1, results; + if (range[1]) { + results = []; + for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) { + results.push(this.highlightLine(lineNumber)); + } + return results; + } else { + return this.highlightLine(range[0]); + } +}; + +// Set the URL hash string +LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { + var hash; + if (lastLineNumber) { + hash = "#L" + firstLineNumber + "-" + lastLineNumber; + } else { + hash = "#L" + firstLineNumber; + } + this._hash = hash; + return this.__setLocationHash__(hash); +}; + +// Make the actual hash change in the browser +// +// This method is stubbed in tests. +LineHighlighter.prototype.__setLocationHash__ = function(value) { + return history.pushState({ + url: value + // We're using pushState instead of assigning location.hash directly to + // prevent the page from scrolling on the hashchange event + }, document.title, value); +}; + +window.LineHighlighter = LineHighlighter; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 7ba676d6d20..6a5084efeb8 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => { return data; }, {}); -let lang = document.querySelector('html').getAttribute('lang') || 'en'; -lang = lang.replace(/-/g, '_'); - +const langAttribute = document.querySelector('html').getAttribute('lang'); +const lang = (langAttribute || 'en').replace(/-/g, '_'); const locale = new Jed(locales[lang]); /** diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0f84470828a..24abc5c5c9e 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -8,6 +8,7 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; import Dropzone from 'dropzone'; import Sortable from 'vendor/Sortable'; +import svg4everybody from 'svg4everybody'; // libraries with import side-effects import 'mousetrap'; @@ -40,7 +41,7 @@ import './commit/image_file'; // lib/utils import './lib/utils/bootstrap_linked_tabs'; -import './lib/utils/common_utils'; +import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; import './lib/utils/pretty_time'; import './lib/utils/text_utility'; @@ -101,7 +102,6 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; -import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; @@ -124,7 +124,6 @@ import './preview_markdown'; import './project'; import './project_avatar'; import './project_find_file'; -import './project_fork'; import './project_import'; import './project_label_subscription'; import './project_new'; @@ -152,6 +151,8 @@ if (process.env.NODE_ENV !== 'production') require('./test_utils/'); Dropzone.autoDiscover = false; +svg4everybody(); + document.addEventListener('beforeunload', function () { // Unbind scroll events $(document).off('scroll'); @@ -161,10 +162,10 @@ document.addEventListener('beforeunload', function () { $('[data-toggle="popover"]').popover('destroy'); }); -window.addEventListener('hashchange', gl.utils.handleLocationHash); +window.addEventListener('hashchange', handleLocationHash); window.addEventListener('load', function onLoad() { window.removeEventListener('load', onLoad, false); - gl.utils.handleLocationHash(); + handleLocationHash(); }, false); gl.lazyLoader = new LazyLoader({ @@ -190,7 +191,7 @@ $(function () { $body.on('click', 'a[href^="#"]', function() { var href = this.getAttribute('href'); if (href.substr(1) === gl.utils.getLocationHash()) { - setTimeout(gl.utils.handleLocationHash, 1); + setTimeout(handleLocationHash, 1); } }); @@ -300,7 +301,10 @@ $(function () { return $container.remove(); // Commit show suppressed diff }); - $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded')); + $('.navbar-toggle').on('click', () => { + $('.header-content').toggleClass('menu-expanded'); + gl.lazyLoader.loadCheck(); + }); // Show/hide comments on diff $body.on('click', '.js-toggle-diff-comments', function (e) { var $this = $(this); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 0c3c877ff15..d3299c15720 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -7,6 +7,11 @@ import './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; +import { + parseUrlPathname, + handleLocationHash, + isMetaClick, +} from './lib/utils/common_utils'; /* eslint-disable max-len */ // MergeRequestTabs @@ -114,7 +119,7 @@ import bp from './breakpoints'; } clickTab(e) { - if (e.currentTarget && gl.utils.isMetaClick(e)) { + if (e.currentTarget && isMetaClick(e)) { const targetLink = e.currentTarget.getAttribute('href'); e.stopImmediatePropagation(); e.preventDefault(); @@ -243,6 +248,8 @@ import bp from './breakpoints'; propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, + errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); @@ -260,7 +267,7 @@ import bp from './breakpoints'; // We extract pathname for the current Changes tab anchor href // some pages like MergeRequestsController#new has query parameters on that anchor - const urlPathname = gl.utils.parseUrlPathname(source); + const urlPathname = parseUrlPathname(source); this.ajaxGet({ url: `${urlPathname}.json${location.search}`, @@ -309,7 +316,7 @@ import bp from './breakpoints'; forceShow: true, }); anchor[0].scrollIntoView(); - window.gl.utils.handleLocationHash(); + handleLocationHash(); // We have multiple elements on the page with `#note_xxx` // (discussion and diff tabs) and `:target` only applies to the first anchor.addClass('target'); @@ -345,7 +352,7 @@ import bp from './breakpoints'; } expandViewContainer() { - const $wrapper = $('.content-wrapper .container-fluid'); + const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); if (this.fixedLayoutPref === null) { this.fixedLayoutPref = $wrapper.hasClass('container-limited'); } diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 5d96b193fce..f80a26b3fd4 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -7,6 +7,7 @@ import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; import eventHub from '../event_hub'; + import { convertPermissionToBoolean } from '../../lib/utils/common_utils'; export default { @@ -17,11 +18,14 @@ return { store, state: 'gettingStarted', - hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), + hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), documentationPath: metricsData.documentationPath, settingsPath: metricsData.settingsPath, metricsEndpoint: metricsData.additionalMetrics, deploymentEndpoint: metricsData.deploymentEndpoint, + emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath, + emptyLoadingSvgPath: metricsData.emptyLoadingSvgPath, + emptyUnableToConnectSvgPath: metricsData.emptyUnableToConnectSvgPath, showEmptyState: true, updateAspectRatio: false, updatedAspectRatios: 0, @@ -108,5 +112,8 @@ :selected-state="state" :documentation-path="documentationPath" :settings-path="settingsPath" + :empty-getting-started-svg-path="emptyGettingStartedSvgPath" + :empty-loading-svg-path="emptyLoadingSvgPath" + :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" /> </template> diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a8708be76de..a7b483f6786 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -1,8 +1,4 @@ <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: { @@ -18,24 +14,36 @@ type: String, required: true, }, + emptyGettingStartedSvgPath: { + type: String, + required: true, + }, + emptyLoadingSvgPath: { + type: String, + required: true, + }, + emptyUnableToConnectSvgPath: { + type: String, + required: true, + }, }, data() { return { states: { gettingStarted: { - svg: gettingStartedSvg, + svgUrl: this.emptyGettingStartedSvgPath, 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, + svgUrl: this.emptyLoadingSvgPath, 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, + svgUrl: this.emptyUnableToConnectSvgPath, title: 'Unable to connect to Prometheus server', description: 'Ensure connectivity is available from the GitLab server to the ', buttonText: 'View documentation', @@ -66,7 +74,9 @@ <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 class="col-md-4 col-md-offset-4 state-svg svg-content"> + <img :src="currentState.svgUrl"/> + </div> </div> <div class="row"> <div class="col-md-6 col-md-offset-3"> diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 4ed651d5740..fed884d5c94 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; import statusCodes from '../../lib/utils/http_status'; +import { backOff } from '../../lib/utils/common_utils'; Vue.use(VueResource); @@ -8,7 +9,7 @@ const MAX_REQUESTS = 3; function backOffRequest(makeRequestCallback) { let requestCounter = 0; - return gl.utils.backOff((next, stop) => { + return backOff((next, stop) => { makeRequestCallback().then((resp) => { if (resp.status === statusCodes.NO_CONTENT) { requestCounter += 1; diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index f2eb2338a1e..997550b37fb 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -11,6 +11,7 @@ export default class NewNavSidebar { initDomElements() { this.$page = $('.page-with-sidebar'); this.$sidebar = $('.nav-sidebar'); + this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$overlay = $('.mobile-overlay'); this.$openSidebar = $('.toggle-mobile-nav'); this.$closeSidebar = $('.close-nav-button'); @@ -55,6 +56,16 @@ export default class NewNavSidebar { this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); + + this.toggleSidebarOverflow(); + } + + toggleSidebarOverflow() { + if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) { + this.$innerScroll.css('overflow-y', 'scroll'); + } else { + this.$innerScroll.css('overflow-y', ''); + } } render() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index f5f7bb4653d..93aa29454a0 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -23,6 +23,7 @@ import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; import TaskList from './task_list'; +import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; window.autosize = autosize; window.Dropzone = Dropzone; @@ -81,7 +82,7 @@ export default class Notes { this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab - if (gl.utils.getPagePath(1) === 'merge_requests') { + if (getPagePath(1) === 'merge_requests') { $('.note-edit-form').clone() .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); } @@ -175,7 +176,7 @@ export default class Notes { keydownNoteText(e) { var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; - if (gl.utils.isMetaKey(e)) { + if (isMetaKey(e)) { return; } @@ -644,10 +645,10 @@ export default class Notes { } else { var $buttons = $el.find('.note-form-actions'); - var isWidgetVisible = gl.utils.isInViewport($el.get(0)); + var isWidgetVisible = isInViewport($el.get(0)); if (!isWidgetVisible) { - gl.utils.scrollToElement($el); + scrollToElement($el); } $el.find('.js-finish-edit-warning').show(); @@ -1188,7 +1189,7 @@ export default class Notes { } static checkMergeRequestStatus() { - if (gl.utils.getPagePath(1) === 'merge_requests') { + if (getPagePath(1) === 'merge_requests') { gl.mrWidget.checkStatus(); } } @@ -1326,7 +1327,7 @@ export default class Notes { * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve * 3) Build temporary placeholder element (using `createPlaceholderNote`) * 4) Show placeholder note on UI - * 5) Perform network request to submit the note using `gl.utils.ajaxPost` + * 5) Perform network request to submit the note using `ajaxPost` * a) If request is successfully completed * 1. Remove placeholder element * 2. Show submitted Note element @@ -1408,7 +1409,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to submit comment on server - gl.utils.ajaxPost(formAction, formData) + ajaxPost(formAction, formData) .then((note) => { // Submission successful! remove placeholder $notesContainer.find(`#${noteUniqueId}`).remove(); @@ -1481,7 +1482,7 @@ export default class Notes { * * 1) Get Form metadata * 2) Update note element with new content - * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost` + * 3) Perform network request to submit the updated note using `ajaxPost` * a) If request is successfully completed * 1. Show submitted Note element * b) If request failed @@ -1510,7 +1511,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to update comment on server - gl.utils.ajaxPost(formAction, formData) + ajaxPost(formAction, formData) .then((note) => { // Submission successful! render final note element this.updateNote(note, $editingNote); diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 16f4e22aa9b..fa7ac994058 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -2,6 +2,7 @@ /* global Flash, Autosave */ import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; + import autosize from 'vendor/autosize'; import '../../autosave'; import TaskList from '../../task_list'; import * as constants from '../constants'; @@ -96,6 +97,8 @@ methods: { ...mapActions([ 'saveNote', + 'stopPolling', + 'restartPolling', 'removePlaceholderNotes', ]), setIsSubmitButtonDisabled(note, isSubmitting) { @@ -124,10 +127,14 @@ } this.isSubmitting = true; this.note = ''; // Empty textarea while being requested. Repopulate in catch + this.resizeTextarea(); + this.stopPolling(); this.saveNote(noteData) .then((res) => { this.isSubmitting = false; + this.restartPolling(); + if (res.errors) { if (res.errors.commands_only) { this.discard(); @@ -174,6 +181,8 @@ if (shouldClear) { this.note = ''; + this.resizeTextarea(); + this.$refs.markdownField.previewMarkdown = false; } // reset autostave @@ -205,6 +214,11 @@ selector: '.notes', }); }, + resizeTextarea() { + this.$nextTick(() => { + autosize.update(this.$refs.textarea); + }); + }, }, mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. @@ -247,7 +261,8 @@ :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :add-spacing-classes="false" - :is-confidential-issue="isConfidentialIssue"> + :is-confidential-issue="isConfidentialIssue" + ref="markdownField"> <textarea id="note-body" name="note[note]" diff --git a/app/assets/javascripts/notes/components/issue_note_icons.js b/app/assets/javascripts/notes/components/issue_note_icons.js deleted file mode 100644 index d8e3cb4bc01..00000000000 --- a/app/assets/javascripts/notes/components/issue_note_icons.js +++ /dev/null @@ -1,37 +0,0 @@ -import iconArrowCircle from 'icons/_icon_arrow_circle_o_right.svg'; -import iconCheck from 'icons/_icon_check_square_o.svg'; -import iconClock from 'icons/_icon_clock_o.svg'; -import iconCodeFork from 'icons/_icon_code_fork.svg'; -import iconComment from 'icons/_icon_comment_o.svg'; -import iconCommit from 'icons/_icon_commit.svg'; -import iconEdit from 'icons/_icon_edit.svg'; -import iconEye from 'icons/_icon_eye.svg'; -import iconEyeSlash from 'icons/_icon_eye_slash.svg'; -import iconMerge from 'icons/_icon_merge.svg'; -import iconMerged from 'icons/_icon_merged.svg'; -import iconRandom from 'icons/_icon_random.svg'; -import iconClosed from 'icons/_icon_status_closed.svg'; -import iconStatusOpen from 'icons/_icon_status_open.svg'; -import iconStopwatch from 'icons/_icon_stopwatch.svg'; -import iconTags from 'icons/_icon_tags.svg'; -import iconUser from 'icons/_icon_user.svg'; - -export default { - icon_arrow_circle_o_right: iconArrowCircle, - icon_check_square_o: iconCheck, - icon_clock_o: iconClock, - icon_code_fork: iconCodeFork, - icon_comment_o: iconComment, - icon_commit: iconCommit, - icon_edit: iconEdit, - icon_eye: iconEye, - icon_eye_slash: iconEyeSlash, - icon_merge: iconMerge, - icon_merged: iconMerged, - icon_random: iconRandom, - icon_status_closed: iconClosed, - icon_status_open: iconStatusOpen, - icon_stopwatch: iconStopwatch, - icon_tags: iconTags, - icon_user: iconUser, -}; diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue index 5bb8f871b9d..0cfb6522e77 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/notes/components/issue_system_note.vue @@ -1,6 +1,5 @@ <script> import { mapGetters } from 'vuex'; - import iconsMap from './issue_note_icons'; import issueNoteHeader from './issue_note_header.vue'; export default { @@ -24,9 +23,9 @@ isTargetNote() { return this.targetNoteHash === this.noteAnchorId; }, - }, - created() { - this.svg = iconsMap[this.note.system_note_icon_name]; + iconHtml() { + return gl.utils.spriteIcon(this.note.system_note_icon_name); + }, }, }; </script> @@ -39,7 +38,7 @@ <div class="timeline-entry-inner"> <div class="timeline-icon" - v-html="svg"> + v-html="iconHtml"> </div> <div class="timeline-content"> <div class="note-header"> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 13cd74bfa1c..1a791039909 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -7,6 +7,7 @@ import * as constants from '../constants'; import service from '../services/issue_notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; +import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; let eTagPoll; @@ -186,6 +187,14 @@ export const poll = ({ commit, state, getters }) => { }); }; +export const stopPolling = () => { + eTagPoll.stop(); +}; + +export const restartPolling = () => { + eTagPoll.restart(); +}; + export const fetchData = ({ commit, state, getters }) => { const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; @@ -211,7 +220,7 @@ export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { }; export const scrollToNoteIfNeeded = (context, el) => { - if (!gl.utils.isInViewport(el[0])) { - gl.utils.scrollToElement(el); + if (!isInViewport(el[0])) { + scrollToElement(el); } }; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 3b2b2089d6e..c2a08f3d6fe 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -5,15 +5,19 @@ import * as constants from '../constants'; export default { [types.ADD_NEW_NOTE](state, note) { const { discussion_id, type } = note; - const noteData = { - expanded: true, - id: discussion_id, - individual_note: !(type === constants.DISCUSSION_NOTE), - notes: [note], - reply_id: discussion_id, - }; - - state.notes.push(noteData); + const [exists] = state.notes.filter(n => n.id === note.discussion_id); + + if (!exists) { + const noteData = { + expanded: true, + id: discussion_id, + individual_note: !(type === constants.DISCUSSION_NOTE), + notes: [note], + reply_id: discussion_id, + }; + + state.notes.push(noteData); + } }, [types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) { diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 01110420cca..e3fc1e2fc2f 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,4 +1,4 @@ -import '~/lib/utils/common_utils'; +import { getParameterByName } from '~/lib/utils/common_utils'; import '~/lib/utils/url_utility'; (() => { @@ -9,7 +9,7 @@ import '~/lib/utils/url_utility'; init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); this.limit = limit; - this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit; + this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.prepareData = prepareData; this.callback = callback; diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js index 644efd10509..9e0e5cacb11 100644 --- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -1,3 +1,5 @@ +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; + function insertRow($row) { const $rowClone = $row.clone(); $rowClone.removeAttr('data-is-persisted'); @@ -6,7 +8,7 @@ function insertRow($row) { } function removeRow($row) { - const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted')); + const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); if (isPersisted) { $row.hide(); diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js index 26a36ad54d1..07abe714367 100644 --- a/app/assets/javascripts/pipelines.js +++ b/app/assets/javascripts/pipelines.js @@ -1,4 +1,5 @@ import LinkedTabs from './lib/utils/bootstrap_linked_tabs'; +import { setCiStatusFavicon } from './lib/utils/common_utils'; export default class Pipelines { constructor(options = {}) { @@ -8,7 +9,7 @@ export default class Pipelines { } if (options.pipelineStatusUrl) { - gl.utils.setCiStatusFavicon(options.pipelineStatusUrl); + setCiStatusFavicon(options.pipelineStatusUrl); } } } diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 3db64339a62..0eaac8dd64f 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -1,21 +1,24 @@ <script> -import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg'; - export default { props: { helpPagePath: { type: String, required: true, }, + emptyStateSvgPath: { + type: String, + required: true, + }, }, - data: () => ({ pipelinesEmptyStateSVG }), }; </script> <template> <div class="row empty-state js-empty-state"> <div class="col-xs-12"> - <div class="svg-content" v-html="pipelinesEmptyStateSVG" /> + <div class="svg-content"> + <img :src="emptyStateSvgPath"/> + </div> </div> <div class="col-xs-12 text-center"> diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue index 90cee68163e..012853b201d 100644 --- a/app/assets/javascripts/pipelines/components/error_state.vue +++ b/app/assets/javascripts/pipelines/components/error_state.vue @@ -1,15 +1,20 @@ <script> -import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg'; - export default { - data: () => ({ pipelinesErrorStateSVG }), + props: { + errorStateSvgPath: { + type: String, + required: true, + }, + }, }; </script> <template> <div class="row empty-state js-pipelines-error-state"> <div class="col-xs-12"> - <div class="svg-content" v-html="pipelinesErrorStateSVG" /> + <div class="svg-content"> + <img :src="errorStateSvgPath"/> + </div> </div> <div class="col-xs-12 text-center"> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index f0b44dfa6d8..76b97af39f1 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -28,8 +28,7 @@ popoverOptions() { return { html: true, - delay: { hide: 600 }, - trigger: 'hover', + trigger: 'focus', placement: 'top', title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, @@ -75,6 +74,7 @@ </span> <a v-if="pipeline.flags.auto_devops" + tabindex="0" class="js-pipeline-url-autodevops label label-info autodevops-badge" v-popover="popoverOptions" role="button"> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 5e6d6b2fbdc..085bd20cefe 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -4,6 +4,7 @@ import tablePagination from '../../vue_shared/components/table_pagination.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; + import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; export default { props: { @@ -26,6 +27,8 @@ return { endpoint: pipelinesData.endpoint, helpPagePath: pipelinesData.helpPagePath, + emptyStateSvgPath: pipelinesData.emptyStateSvgPath, + errorStateSvgPath: pipelinesData.errorStateSvgPath, autoDevopsPath: pipelinesData.helpAutoDevopsPath, newPipelinePath: pipelinesData.newPipelinePath, canCreatePipeline: pipelinesData.canCreatePipeline, @@ -44,10 +47,10 @@ }, computed: { canCreatePipelineParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); + return convertPermissionToBoolean(this.canCreatePipeline); }, scope() { - const scope = gl.utils.getParameterByName('scope'); + const scope = getParameterByName('scope'); return scope === null ? 'all' : scope; }, @@ -105,10 +108,10 @@ }; }, pageParameter() { - return gl.utils.getParameterByName('page') || this.pagenum; + return getParameterByName('page') || this.pagenum; }, scopeParameter() { - return gl.utils.getParameterByName('scope') || this.apiScope; + return getParameterByName('scope') || this.apiScope; }, }, created() { @@ -122,7 +125,7 @@ * @param {Number} pageNumber desired page to go to. */ change(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; @@ -181,9 +184,13 @@ <empty-state v-if="shouldRenderEmptyState" :help-page-path="helpPagePath" + :empty-state-svg-path="emptyStateSvgPath" /> - <error-state v-if="shouldRenderErrorState" /> + <error-state + v-if="shouldRenderErrorState" + :error-state-svg-path="errorStateSvgPath" + /> <div class="blank-state blank-state-no-icon" diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 01dfe51cc17..c4c63a52358 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -69,8 +69,7 @@ @click="onClickAction(action.path)" :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)"> - <span v-html="playIconSvg"></span> - <span>{{action.name}}</span> + {{action.name}} </button> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index b19bd509a00..751a20991af 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -39,11 +39,7 @@ rel="nofollow" download :href="artifact.path"> - <i - class="fa fa-download" - aria-hidden="true"> - </i> - <span>Download {{artifact.name}} artifacts</span> + Download {{artifact.name}} artifacts </a> </li> </ul> diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 9adc15e6266..e97f5632dc8 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -97,7 +97,7 @@ export default { postAction(endpoint) { this.service.postAction(endpoint) .then(() => eventHub.$emit('refreshPipelines')) - .catch(() => new Flash('An error occured while making the request.')); + .catch(() => new Flash('An error occurred while making the request.')); }, }, }; diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index ffefe0192f2..651251d2623 100644 --- a/app/assets/javascripts/pipelines/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js @@ -1,3 +1,5 @@ +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; + export default class PipelinesStore { constructor() { this.state = {}; @@ -19,8 +21,8 @@ export default class PipelinesStore { let paginationInfo; if (Object.keys(pagination).length) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + paginationInfo = parseIntPagination(normalizedHeaders); } else { paginationInfo = pagination; } diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index 291ae24aa68..4bdda611cfc 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -73,7 +73,8 @@ import _ from 'underscore'; aspectRatio: 1, modal: true, scalable: false, - rotatable: false, + rotatable: true, + checkOrientation: true, zoomable: true, dragMode: 'move', guides: false, diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 4ccea0624ee..3deb242bc1f 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ /* global Flash */ +import { getPagePath } from '../lib/utils/common_utils'; ((global) => { class Profile { @@ -93,7 +94,7 @@ return $title.val(comment[1]).change(); } }); - if (global.utils.getPagePath() === 'profiles') { + if (getPagePath() === 'profiles') { return new Profile(); } }); diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 47197db39d3..68cf47fd54e 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -1,13 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ -(function() { - this.ProjectFork = (function() { - function ProjectFork() { - $('.fork-thumbnail a').on('click', function() { - $('.fork-namespaces').hide(); - return $('.save-project-loader').show(); - }); - } +export default () => { + $('.fork-thumbnail a').on('click', function forkThumbnailClicked() { + if ($(this).hasClass('disabled')) return false; - return ProjectFork; - })(); -}).call(window); + $('.fork-namespaces').hide(); + return $('.save-project-loader').show(); + }); +}; diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index ef4d6df5138..a4d50a52315 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -1,4 +1,5 @@ import PANEL_STATE from './constants'; +import { backOff } from '../lib/utils/common_utils'; export default class PrometheusMetrics { constructor(wrapperSelector) { @@ -79,7 +80,7 @@ export default class PrometheusMetrics { loadActiveMetrics() { this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); - gl.utils.backOff((next, stop) => { + backOff((next, stop) => { $.getJSON(this.activeMetricsEndpoint) .done((res) => { if (res && res.success) { diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 1282828b504..119e38c583d 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -37,14 +37,14 @@ export default { content: f.newContent, })); const payload = { - branch: Store.targetBranch, + branch: Store.currentBranch, commit_message: commitMessage, actions, }; Store.submitCommitsLoading = true; Service.commitFiles(payload) .then(this.resetCommitState) - .catch(() => Flash('An error occured while committing your changes')); + .catch(() => Flash('An error occurred while committing your changes')); }, resetCommitState() { @@ -105,7 +105,7 @@ export default { </label> <div class="col-md-6"> <span class="help-block"> - {{targetBranch}} + {{currentBranch}} </span> </div> </div> diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index 29b76975561..353142edeb7 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -26,16 +26,6 @@ export default { this.editMode = !this.editMode; Store.toggleBlobView(); }, - toggleProjectRefsForm() { - $('.project-refs-form').toggleClass('disabled', this.editMode); - $('.js-tree-ref-target-holder').toggle(this.editMode); - }, - }, - - watch: { - editMode() { - this.toggleProjectRefsForm(); - }, }, }; </script> diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index 20ebf840774..8b9cbd23456 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -95,7 +95,7 @@ export default RepoFile; </div> </td> - <td class="hidden-xs"> + <td class="hidden-xs text-right"> <span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)"> diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index 2200754cbef..2fe369a4693 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -1,23 +1,27 @@ <script> +/* global LineHighlighter */ + import Store from '../stores/repo_store'; export default { data: () => Store, - mounted() { - this.highlightFile(); - }, computed: { html() { return this.activeFile.html; }, }, - methods: { highlightFile() { $(this.$el).find('.file-content').syntaxHighlight(); }, }, - + mounted() { + this.highlightFile(); + this.lineHighlighter = new LineHighlighter({ + fileHolderSelector: '.blob-viewer-container', + scrollFileHolder: true, + }); + }, watch: { html() { this.$nextTick(() => { @@ -45,7 +49,7 @@ export default { v-else class="vertical-center render-error"> <p class="text-center"> - The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead. + The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead. </p> </div> </div> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 3414128526d..1e40814b95f 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -37,17 +37,24 @@ export default { let file = clickedFile; if (file.loading) return; file.loading = true; + if (file.type === 'tree' && file.opened) { file = Store.removeChildFilesOfTree(file); file.loading = false; } else { - Service.url = file.url; - Helper.getContent(file) - .then(() => { - file.loading = false; - Helper.scrollTabsRight(); - }) - .catch(Helper.loadingError); + const openFile = Helper.getFileFromPath(file.url); + if (openFile) { + file.loading = false; + Store.setActiveFiles(openFile); + } else { + Service.url = file.url; + Helper.getContent(file) + .then(() => { + file.loading = false; + Helper.scrollTabsRight(); + }) + .catch(Helper.loadingError); + } } }, @@ -68,7 +75,7 @@ export default { <tr> <th class="name">Name</th> <th class="hidden-sm hidden-xs last-commit">Last Commit</th> - <th class="hidden-xs last-update">Last Update</th> + <th class="hidden-xs last-update text-right">Last Update</th> </tr> </thead> <tbody> diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index 2bd8d7eea65..ac59a2bed23 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -58,13 +58,13 @@ const RepoHelper = { return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1); }, - setDirectoryOpen(tree) { + setDirectoryOpen(tree, title) { const file = tree; if (!file) return undefined; file.opened = true; file.icon = 'fa-folder-open'; - RepoHelper.updateHistoryEntry(file.url, file.name); + RepoHelper.updateHistoryEntry(file.url, title); return file; }, @@ -135,6 +135,8 @@ const RepoHelper = { return Service.getContent() .then((response) => { const data = response.data; + if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title']; + Store.isTree = RepoHelper.isTree(data); if (!Store.isTree) { if (!file) file = data; @@ -168,7 +170,7 @@ const RepoHelper = { } else { // it's a tree if (!file) Store.isRoot = RepoHelper.isRoot(Service.url); - file = RepoHelper.setDirectoryOpen(file); + file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name); const newDirectory = RepoHelper.dataToListOfFiles(data); Store.addFilesToDirectory(file, Store.files, newDirectory); Store.prevURL = Service.blobURLtoParentTree(Service.url); @@ -178,8 +180,8 @@ const RepoHelper = { setFile(data, file) { const newFile = data; + newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh. - newFile.url = file.url; if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { newFile.tooLarge = true; } @@ -255,7 +257,7 @@ const RepoHelper = { history.pushState({ key: RepoHelper.key }, '', url); if (title) { - document.title = `${title} · GitLab`; + document.title = title; } }, @@ -263,6 +265,10 @@ const RepoHelper = { return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); }, + getFileFromPath(path) { + return Store.openedFiles.find(file => file.url === path); + }, + loadingError() { Flash('Unable to load this content at this time.'); }, diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 6c1d468e937..7d0123e3d3a 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -11,10 +11,6 @@ function initDropdowns() { } function addEventsForNonVueEls() { - $(document).on('change', '.dropdown', () => { - Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val(); - }); - window.onbeforeunload = function confirmUnload(e) { const hasChanged = Store.openedFiles .some(file => file.changed); diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 1c0df528aea..9a4fc40bc69 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -32,7 +32,6 @@ const RepoStore = { isCommitable: false, binary: false, currentBranch: '', - targetBranch: 'new-branch', commitMessage: '', binaryTypes: { png: false, @@ -84,7 +83,7 @@ const RepoStore = { }).catch(Helper.loadingError); } - if (!file.loading) Helper.updateHistoryEntry(file.url, file.name); + if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); RepoStore.binary = file.binary; }, diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index a4eae135403..0c1ec276baf 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -43,6 +43,8 @@ import Cookies from 'js-cookie'; $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + + if (gl.lazyLoader) gl.lazyLoader.loadCheck(); } if (!triggered) { return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 003a15592f3..f15452ec683 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,4 +1,5 @@ /* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ +import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; ((global) => { const KEYCODE = { @@ -146,14 +147,14 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName; userId = gon.current_user_id; userName = gon.current_username; - utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; - if (utils.isInGroupsPage() && groupOptions) { - options = groupOptions[utils.getGroupSlug()]; - } else if (utils.isInProjectPage() && projectOptions) { - options = projectOptions[utils.getProjectSlug()]; + projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; + if (isInGroupsPage() && groupOptions) { + options = groupOptions[getGroupSlug()]; + } else if (isInProjectPage() && projectOptions) { + options = projectOptions[getProjectSlug()]; } else if (dashboardOptions) { options = dashboardOptions; } @@ -286,6 +287,7 @@ onClearInputClick(e) { e.preventDefault(); + this.wrap.toggleClass('has-value', !!e.target.value); return this.searchInput.val('').focus(); } diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js index 0da265053bd..a9fbc7f1a2f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js @@ -1,6 +1,5 @@ import stopwatchSvg from 'icons/_icon_stopwatch.svg'; - -import '../../../lib/utils/pretty_time'; +import { abbreviateTime } from '../../../lib/utils/pretty_time'; export default { name: 'time-tracking-collapsed-state', @@ -79,7 +78,7 @@ export default { }, methods: { abbreviateTime(timeStr) { - return gl.utils.prettyTime.abbreviateTime(timeStr); + return abbreviateTime(timeStr); }, }, template: ` diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js index 40f5c89c5bb..fd0d4570d68 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js @@ -1,6 +1,4 @@ -import '../../../lib/utils/pretty_time'; - -const prettyTime = gl.utils.prettyTime; +import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; export default { name: 'time-tracking-comparison-pane', @@ -23,12 +21,12 @@ export default { }, }, computed: { - parsedRemaining() { + parsedTimeRemaining() { const diffSeconds = this.timeEstimate - this.timeSpent; - return prettyTime.parseSeconds(diffSeconds); + return parseSeconds(diffSeconds); }, timeRemainingHumanReadable() { - return prettyTime.stringifyTime(this.parsedRemaining); + return stringifyTime(this.parsedTimeRemaining); }, timeRemainingTooltip() { const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; @@ -44,13 +42,6 @@ export default { timeRemainingStatusClass() { return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; }, - /* Parsed time values */ - parsedEstimate() { - return prettyTime.parseSeconds(this.timeEstimate); - }, - parsedSpent() { - return prettyTime.parseSeconds(this.timeSpent); - }, }, template: ` <div class="time-tracking-comparison-pane"> diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 1c15a1b877a..3c9de02407e 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -38,7 +38,7 @@ class SidebarMoveIssue { data: (searchTerm, callback) => { this.mediator.fetchAutocompleteProjects(searchTerm) .then(callback) - .catch(() => new Flash('An error occured while fetching projects autocomplete.')); + .catch(() => new Flash('An error occurred while fetching projects autocomplete.')); }, renderRow: project => ` <li> @@ -73,7 +73,7 @@ class SidebarMoveIssue { this.mediator.moveIssue() .catch(() => { - Flash('An error occured while moving the issue.'); + Flash('An error occurred while moving the issue.'); this.$confirmButton .enable() .removeClass('is-loading'); diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index e38a8db4cc5..2fe6e5b31f0 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -41,7 +41,7 @@ export default class SidebarMediator { this.store.setAssigneeData(data); this.store.setTimeTrackingData(data); }) - .catch(() => new Flash('Error occured when fetching sidebar data')); + .catch(() => new Flash('Error occurred when fetching sidebar data')); } fetchAutocompleteProjects(searchTerm) { diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index a606852c22c..2fffe09c74e 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ import UsersSelect from './users_select'; +import { isMetaClick } from './lib/utils/common_utils'; export default class Todos { constructor() { @@ -137,22 +138,17 @@ export default class Todos { goToTodoUrl(e) { const todoLink = this.dataset.url; - if (!todoLink) { + if (!todoLink || e.target.tagName === 'A' || e.target.tagName === 'IMG') { return; } - if (gl.utils.isMetaClick(e)) { + e.stopPropagation(); + e.preventDefault(); + + if (isMetaClick(e)) { const windowTarget = '_blank'; - const selected = e.target; - e.stopPropagation(); - e.preventDefault(); - - if (selected.tagName === 'IMG') { - const avatarUrl = selected.parentElement.getAttribute('href'); - window.open(avatarUrl, windowTarget); - } else { - window.open(todoLink, windowTarget); - } + + window.open(todoLink, windowTarget); } else { gl.utils.visitUrl(todoLink); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index a4e34116c33..a8c686e5065 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -1,6 +1,6 @@ import statusCodes from '../../lib/utils/http_status'; import { bytesToMiB } from '../../lib/utils/number_utils'; - +import { backOff } from '../../lib/utils/common_utils'; import MemoryGraph from '../../vue_shared/components/memory_graph'; import MRWidgetService from '../services/mr_widget_service'; @@ -84,7 +84,7 @@ export default { } }, loadMetrics() { - gl.utils.backOff((next, stop) => { + backOff((next, stop) => { MRWidgetService.fetchMetrics(this.metricsUrl) .then((res) => { if (res.status === statusCodes.NO_CONTENT) { diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 2f237262028..044b664484b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -31,6 +31,7 @@ import { SquashBeforeMerge, notify, } from './dependencies'; +import { setFavicon } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -86,7 +87,7 @@ export default { .then((res) => { this.handleNotification(res); this.mr.setData(res); - this.setFavicon(); + this.setFaviconHelper(); if (cb) { cb.call(null, res); @@ -115,9 +116,9 @@ export default { immediateExecution: true, }); }, - setFavicon() { + setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - gl.utils.setFavicon(this.mr.ciStatusFaviconPath); + setFavicon(this.mr.ciStatusFaviconPath); } }, fetchDeployments() { @@ -193,7 +194,7 @@ export default { }); }, handleMounted() { - this.setFavicon(); + this.setFaviconHelper(); this.initDeploymentsPolling(); }, }, diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index c9dbc048345..710452bb3d3 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -1,11 +1,13 @@ <script> +import { s__ } from '../../locale'; + const PAGINATION_UI_BUTTON_LIMIT = 4; const UI_LIMIT = 6; const SPREAD = '...'; -const PREV = 'Prev'; -const NEXT = 'Next'; -const FIRST = '« First'; -const LAST = 'Last »'; +const PREV = s__('Pagination|Prev'); +const NEXT = s__('Pagination|Next'); +const FIRST = s__('Pagination|« First'); +const LAST = s__('Pagination|Last »'); export default { props: { diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index 7f8e514fda1..b9693892f45 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; +import csrf from '../lib/utils/csrf'; Vue.use(VueResource); @@ -18,9 +19,7 @@ Vue.http.interceptors.push((request, next) => { // New Vue Resource version uses Headers, we are expecting a plain object to render pagination // and polling. Vue.http.interceptors.push((request, next) => { - if ($.rails) { - request.headers.set('X-CSRF-Token', $.rails.csrfToken()); - } + request.headers.set(csrf.headerKey, csrf.token); next((response) => { // Headers object has a `forEach` property that iterates through all values. diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 35e7a10379f..74b846217bb 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -31,10 +31,12 @@ @import "framework/mobile"; @import "framework/modal"; @import "framework/nav"; +@import "framework/new-nav"; @import "framework/pagination"; @import "framework/panels"; @import "framework/selects"; @import "framework/sidebar"; +@import "framework/new-sidebar"; @import "framework/tables"; @import "framework/notes"; @import "framework/timeline"; @@ -52,4 +54,3 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive-tables"; -@import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index bb30da4f4b2..e0d2ed80de5 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -9,6 +9,7 @@ } .emoji-menu { + display: none; position: absolute; top: 0; margin-top: 3px; @@ -27,6 +28,10 @@ transition: .3s cubic-bezier(.67, .06, .19, 1.44); transition-property: transform, opacity; + &.is-rendered { + display: block; + } + &.is-aligned-right { transform-origin: 100% -45px; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index b575ec9de18..1d72a70f0f5 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -260,7 +260,7 @@ position: relative; border: 1px solid $blue-300; border-radius: $border-radius-default; - background-color: $blue-25; + background-color: $blue-50; justify-content: center; .dismiss-button { @@ -319,16 +319,6 @@ padding: $gl-padding; } - .svg-content { - text-align: center; - - svg { - max-width: 425px; - width: 100%; - padding: $gl-padding; - } - } - .emoji-icon { display: inline-block; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 4f208df4216..d178bc17462 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -46,15 +46,6 @@ } } -@mixin btn-svg { - svg { - height: 15px; - width: 15px; - position: relative; - top: 2px; - } -} - @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; @@ -132,7 +123,6 @@ .btn { @include btn-default; @include btn-white; - @include btn-svg; color: $gl-text-color; @@ -231,6 +221,13 @@ } } + svg { + height: 15px; + width: 15px; + position: relative; + top: 2px; + } + svg, .fa { &:not(:last-child) { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2bcd23a15e6..c0d8e6c328c 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -779,6 +779,14 @@ white-space: normal; width: 100%; + &.dropdown-menu-user-link { + white-space: nowrap; + + .dropdown-menu-user-username { + display: block; + } + } + // make sure the text color is not overriden &.text-danger { color: $brand-danger; diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss deleted file mode 100644 index ebae473df50..00000000000 --- a/app/assets/stylesheets/framework/feature_highlight.scss +++ /dev/null @@ -1,94 +0,0 @@ -.feature-highlight { - position: relative; - margin-left: $gl-padding; - width: 20px; - height: 20px; - cursor: pointer; - - &::before { - content: ''; - display: block; - position: absolute; - top: 6px; - left: 6px; - width: 8px; - height: 8px; - background-color: $blue-500; - border-radius: 50%; - box-shadow: 0 0 0 rgba($blue-500, 0.4); - animation: pulse-highlight 2s infinite; - } - - &:hover::before, - &.disable-animation::before { - animation: none; - } - - &[disabled]::before { - display: none; - } -} - -.is-showing-fly-out { - .feature-highlight { - display: none; - } -} - -.feature-highlight-popover-content { - display: none; - - hr { - margin: $gl-padding * 0.5 0; - } - - .btn-link { - @include btn-svg; - - svg path { - fill: currentColor; - } - } - - .dismiss-feature-highlight { - padding: 0; - } - - svg:first-child { - width: 100%; - background-color: $indigo-50; - border-top-left-radius: 2px; - border-top-right-radius: 2px; - border-bottom: 1px solid darken($gray-normal, 8%); - } -} - -.popover .feature-highlight-popover-content { - display: block; -} - -.feature-highlight-popover { - padding: 0; - - .popover-content { - padding: 0; - } -} - -.feature-highlight-popover-sub-content { - padding: 9px 14px; -} - -@include keyframes(pulse-highlight) { - 0% { - box-shadow: 0 0 0 0 rgba($blue-200, 0.4); - } - - 70% { - box-shadow: 0 0 0 10px transparent; - } - - 100% { - box-shadow: 0 0 0 0 transparent; - } -} diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index f844d6f1d5a..6b69e8018be 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -6,7 +6,7 @@ // Header header.navbar-gitlab-new { - background: linear-gradient(to right, $color-900, $color-800); + background-color: $color-900; .navbar-collapse { color: $color-200; @@ -126,7 +126,7 @@ .search-input-wrap { .search-icon, .clear-icon { - color: rgba($color-200, .8); + fill: rgba($color-200, .8); } } @@ -141,7 +141,7 @@ .search-input-wrap { .search-icon { - color: rgba($color-200, .8); + fill: rgba($color-200, .8); } } } @@ -201,7 +201,7 @@ body { @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); header.navbar-gitlab-new { - background: $theme-gray-100; + background-color: $theme-gray-100; box-shadow: 0 2px 0 0 $border-color; .logo-text svg { @@ -242,17 +242,21 @@ body { &:hover { background-color: $white-light; - box-shadow: inset 0 0 0 1px $blue-100; + box-shadow: inset 0 0 0 1px $blue-200; .location-badge { - box-shadow: inset 0 0 0 1px $blue-100; + box-shadow: inset 0 0 0 1px $blue-200; } } } .search-input-wrap { .search-icon { - color: $theme-gray-200; + fill: $theme-gray-200; + } + + .search-input { + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index ab3c34df1fb..d932ea8794f 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -109,8 +109,7 @@ header { .user-counter { svg { - height: 16px; - width: 23px; + margin-right: 3px; } } @@ -133,16 +132,16 @@ header { } &.navbar-gitlab-new { - .fa-times { + .close-icon { display: none; } .menu-expanded { - .fa-ellipsis-v { + .more-icon { display: none; } - .fa-times { + .close-icon { display: block; } } diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 09a569ad415..59bfc5a8d77 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -9,3 +9,29 @@ padding: 10px; margin-bottom: 10px; } + +.svg-content { + text-align: center; + padding: $gl-padding; + + svg, + img { + max-width: 425px; + width: 100%; + } +} + +@mixin svg-size($size) { + width: $size; + height: $size; +} + +svg { + &.s8 { @include svg-size(8px); } + &.s16 { @include svg-size(16px); } + &.s18 { @include svg-size(18px); } + &.s24 { @include svg-size(24px); } + &.s32 { @include svg-size(32px); } + &.s48 { @include svg-size(48px); } + &.s72 { @include svg-size(72px); } +} diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index d40b65bb2cc..2fee2164190 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -142,5 +142,41 @@ } @mixin green-status-color { - @include status-color($green-50, $green-500, $green-700); + @include status-color($green-100, $green-500, $green-700); +} + +@mixin fade($gradient-direction, $gradient-color) { + visibility: hidden; + opacity: 0; + z-index: 2; + position: absolute; + bottom: 12px; + width: 43px; + height: 30px; + transition-duration: .3s; + -webkit-transform: translateZ(0); + background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4)); + + &.scrolling { + visibility: visible; + opacity: 1; + transition-duration: .3s; + } + + .fa { + position: relative; + top: 5px; + font-size: 18px; + } +} + +@mixin scrolling-links() { + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + display: flex; + + &::-webkit-scrollbar { + display: none; + } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 2f7717760ec..f8777d1fd9d 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,38 +1,4 @@ -@mixin fade($gradient-direction, $gradient-color) { - visibility: hidden; - opacity: 0; - z-index: 2; - position: absolute; - bottom: 12px; - width: 43px; - height: 30px; - transition-duration: .3s; - -webkit-transform: translateZ(0); - background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4)); - - &.scrolling { - visibility: visible; - opacity: 1; - transition-duration: .3s; - } - - .fa { - position: relative; - top: 5px; - font-size: 18px; - } -} -@mixin scrolling-links() { - overflow-x: auto; - overflow-y: hidden; - -webkit-overflow-scrolling: touch; - display: flex; - - &::-webkit-scrollbar { - display: none; - } -} .nav-links { display: flex; diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/framework/new-nav.scss index 58e205537ef..3abf3e4ac7d 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/framework/new-nav.scss @@ -120,17 +120,24 @@ header.navbar-gitlab-new { .container-fluid { .navbar-toggle { min-width: 45px; - padding: 4px $gl-padding; + padding: 0 $gl-padding; margin-right: -7px; - font-size: 14px; text-align: center; color: currentColor; + svg { + fill: currentColor; + } + &:hover, &:focus, &.active { color: currentColor; background-color: transparent; + + svg { + fill: currentColor; + } } } @@ -279,10 +286,6 @@ header.navbar-gitlab-new { } } -.admin-icon i { - font-size: 18px; -} - .caret-down { height: 11px; width: 11px; @@ -295,75 +298,6 @@ header.navbar-gitlab-new { margin-top: 4px; } -.search { - margin: 4px 8px 0; - - form { - height: 32px; - border: 0; - border-radius: $border-radius-default; - transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; - - &:hover { - box-shadow: none; - } - } - - &.search-active form { - box-shadow: none; - - .search-input { - color: $gl-text-color; - transition: color ease-in-out 0.15s; - } - - .search-input::placeholder { - color: $gl-text-color-tertiary; - } - - .search-input-wrap { - .search-icon, - .clear-icon { - color: $gl-text-color-tertiary; - transition: color ease-in-out 0.15s; - } - } - } - - .search-input { - color: $white-light; - background: none; - transition: color ease-in-out 0.15s; - } - - .search-input::placeholder { - transition: color ease-in-out 0.15s; - } - - .location-badge { - font-size: 12px; - margin: -4px 4px -4px -4px; - line-height: 25px; - padding: 4px 8px; - border-radius: 2px 0 0 2px; - height: 32px; - transition: border-color ease-in-out 0.15s; - } - - &.search-active { - .location-badge { - background-color: $nav-badge-bg; - border-color: $border-color; - } - - .search-input-wrap { - .clear-icon { - color: $white-light; - } - } - } -} - .breadcrumbs { display: flex; min-height: 48px; @@ -375,8 +309,8 @@ header.navbar-gitlab-new { display: flex; width: 100%; position: relative; - padding-top: $gl-padding; - padding-bottom: $gl-padding; + padding-top: $gl-padding / 2; + padding-bottom: $gl-padding / 2; align-items: center; border-bottom: 1px solid $border-color; } @@ -417,6 +351,7 @@ header.navbar-gitlab-new { display: flex; align-items: center; position: relative; + padding: 2px 0; &:not(:last-child) { margin-right: 20px; @@ -452,7 +387,7 @@ header.navbar-gitlab-new { margin: 0; font-size: 12px; font-weight: 600; - line-height: 1; + line-height: 16px; a { color: $gl-text-color; diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss index 8030854e527..8332cec2962 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/framework/new-sidebar.scss @@ -56,8 +56,8 @@ $new-sidebar-collapsed-width: 50px; color: $hover-color; .settings-avatar { - i { - color: $hover-color; + svg { + fill: $hover-color; } } } @@ -76,12 +76,9 @@ $new-sidebar-collapsed-width: 50px; .settings-avatar { background-color: $white-light; - i { - font-size: 20px; - width: 100%; - color: $gl-text-color-secondary; - text-align: center; - align-self: center; + svg { + fill: $gl-text-color-secondary; + margin: auto; } } @@ -177,22 +174,26 @@ $new-sidebar-collapsed-width: 50px; .nav-icon-container { display: flex; margin-right: 8px; - - svg { - height: 16px; - width: 16px; - } } .fly-out-top-item { display: none; } + + svg { + height: 16px; + width: 16px; + } } .nav-sidebar-inner-scroll { height: 100%; width: 100%; - overflow: scroll; + overflow: auto; + + @media (min-width: $screen-sm-min) { + overflow: hidden; + } } .with-performance-bar .nav-sidebar { @@ -350,18 +351,22 @@ $new-sidebar-collapsed-width: 50px; display: flex; align-items: center; - i { - font-size: 20px; + svg { + fill: $gl-text-color-secondary; margin-right: 8px; } - .fa-angle-double-right { + .icon-angle-double-right { display: none; } &:hover { background-color: $border-color; color: $gl-text-color; + + svg { + fill: $gl-text-color; + } } } @@ -403,15 +408,16 @@ $new-sidebar-collapsed-width: 50px; .toggle-sidebar-button { width: $new-sidebar-collapsed-width - 2px; - padding: 16px 18px; + padding: 16px; .collapse-text, - .fa-angle-double-left { + .icon-angle-double-left { display: none; } - .fa-angle-double-right { + .icon-angle-double-right { display: block; + margin: 0; } } } @@ -441,9 +447,8 @@ $new-sidebar-collapsed-width: 50px; background-color: transparent; border: 0; padding: 6px 16px; - margin: 0 16px 0 -15px; + margin: 0 0 0 -15px; height: 46px; - border-right: 1px solid $gl-text-color-quaternary; i { font-size: 20px; @@ -451,7 +456,19 @@ $new-sidebar-collapsed-width: 50px; } @media (max-width: $screen-xs-max) { - display: inline-block; + display: flex; + align-items: center; + + i { + font-size: 18px; + } + } + + @media (max-width: $screen-xs-max) { + + .breadcrumbs-links { + padding-left: 17px; + border-left: 1px solid $gl-text-color-quaternary; + } } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 4c35e3a9c3c..3ea77eb7a43 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -137,7 +137,7 @@ $well-border: #eee; //## $code-color: $red-600; -$code-bg: lighten($red-50, 2%); +$code-bg: lighten($red-100, 2%); $kbd-color: $white-light; $kbd-bg: #333; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index a3da9fd44e8..e8bb42f4a8c 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -6,6 +6,8 @@ $gutter_width: 290px; $gutter_inner_width: 250px; $sidebar-transition-duration: .15s; $sidebar-breakpoint: 1024px; +$default-transition-duration: .15s; +$right-sidebar-transition-duration: .3s; /* * Color schema @@ -27,46 +29,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c4c4c4; -$green-25: #f6fcf8; -$green-50: #e4f5eb; -$green-100: #bae6cc; -$green-200: #8dd5aa; -$green-300: #5fc488; -$green-400: #3cb76f; +$green-50: #f1fdf6; +$green-100: #dcf5e7; +$green-200: #b3e6c8; +$green-300: #75d09b; +$green-400: #37b96d; $green-500: #1aaa55; $green-600: #168f48; $green-700: #12753a; $green-800: #0e5a2d; $green-900: #0a4020; +$green-950: #072b15; -$blue-25: #f6fafd; -$blue-50: #e4eff9; -$blue-100: #bcd7f1; -$blue-200: #8fbce8; -$blue-300: #62a1df; -$blue-400: #418cd8; +$blue-50: #f6fafe; +$blue-100: #e4f0fb; +$blue-200: #b8d6f4; +$blue-300: #73afea; +$blue-400: #2e87e0; $blue-500: #1f78d1; $blue-600: #1b69b6; $blue-700: #17599c; $blue-800: #134a81; $blue-900: #0f3b66; +$blue-950: #0a2744; -$orange-25: #fffcf8; -$orange-50: #fff2e1; -$orange-100: #fedfb3; -$orange-200: #feca81; -$orange-300: #fdb44f; -$orange-400: #fca429; +$orange-50: #fffaf4; +$orange-100: #fff1de; +$orange-200: #fed69f; +$orange-300: #fdbc60; +$orange-400: #fca121; $orange-500: #fc9403; $orange-600: #de7e00; $orange-700: #c26700; -$orange-800: #a35100; -$orange-900: #853b00; +$orange-800: #a35200; +$orange-900: #853c00; +$orange-950: #592800; -$red-25: #fef7f6; -$red-50: #fbe7e4; -$red-100: #f4c4bc; -$red-200: #ed9d90; +$red-50: #fef6f5; +$red-100: #fbe5e1; +$red-200: #f2b4a9; $red-300: #e67664; $red-400: #e05842; $red-500: #db3b21; @@ -74,6 +75,7 @@ $red-600: #c0341d; $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; +$red-950: #4b140b; // GitLab themes @@ -184,8 +186,8 @@ $list-text-disabled-color: $gl-text-color-tertiary; $list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; -$list-warning-row-bg: $orange-50; -$list-warning-row-border: $orange-100; +$list-warning-row-bg: $orange-100; +$list-warning-row-border: $orange-200; $list-warning-row-color: $orange-700; /* @@ -214,8 +216,8 @@ $gl-sidebar-padding: 22px; /* * Misc */ -$row-hover: $blue-25; -$row-hover-border: $blue-100; +$row-hover: $blue-50; +$row-hover-border: $blue-200; $progress-color: #c0392b; $header-height: 50px; $new-navbar-height: 40px; @@ -265,8 +267,8 @@ $time-color: #999; $project-member-show-color: #aaa; $gl-promo-color: #aaa; $error-bg: $red-400; -$warning-message-bg: $orange-50; -$warning-message-border: $orange-100; +$warning-message-bg: $orange-100; +$warning-message-border: $orange-200; $warning-message-color: $orange-700; $control-group-descr-color: #666; $table-permission-x-bg: #d9edf7; @@ -451,17 +453,17 @@ $builds-trace-bg: #111; /* * Callout */ -$callout-danger-bg: $red-50; -$callout-danger-border: $red-100; +$callout-danger-bg: $red-100; +$callout-danger-border: $red-200; $callout-danger-color: $red-700; -$callout-warning-bg: $orange-50; -$callout-warning-border: $orange-100; +$callout-warning-bg: $orange-100; +$callout-warning-border: $orange-200; $callout-warning-color: $orange-700; -$callout-info-bg: $blue-50; -$callout-info-border: $blue-100; +$callout-info-bg: $blue-100; +$callout-info-border: $blue-200; $callout-info-color: $blue-700; -$callout-success-bg: $green-50; -$callout-success-border: $green-100; +$callout-success-bg: $green-100; +$callout-success-border: $green-200; $callout-success-color: $green-700; /* diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss new file mode 100644 index 00000000000..6c555aee20a --- /dev/null +++ b/app/assets/stylesheets/pages/admin.scss @@ -0,0 +1,6 @@ +.info-well { + .admin-well-statistics, + .admin-well-features { + padding-bottom: 46px; + } +} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 700be173039..3305a482a0d 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -55,6 +55,15 @@ .boards-app { position: relative; + + @media (min-width: $screen-sm-min) { + transition: width $right-sidebar-transition-duration; + width: 100%; + + &.is-compact { + width: calc(100% - #{$gutter_width}); + } + } } .boards-app-loading { @@ -78,11 +87,6 @@ height: calc(100vh - 222px); // scss-lint:enable DuplicateProperty min-height: 475px; - transition: width .2s; - - &.is-compact { - width: calc(100% - 290px); - } } } @@ -412,14 +416,6 @@ .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar, .page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { - position: absolute; - - &.right-sidebar { - top: 0; - bottom: 0; - height: 100%; - } - .issuable-sidebar-header { position: relative; } @@ -457,8 +453,8 @@ .right-sidebar.right-sidebar-expanded { &.boards-sidebar-slide-enter-active, &.boards-sidebar-slide-leave-active { - transition: width .2s, - padding .2s; + transition: width $right-sidebar-transition-duration, + padding $right-sidebar-transition-duration; } &.boards-sidebar-slide-enter, diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss index 16702442f50..fb1899284fd 100644 --- a/app/assets/stylesheets/pages/convdev_index.scss +++ b/app/assets/stylesheets/pages/convdev_index.scss @@ -83,7 +83,7 @@ $space-between-cards: 8px; border-top-color: $color-low-score; .card-score-big { - background-color: $red-25; + background-color: $red-50; } } @@ -91,7 +91,7 @@ $space-between-cards: 8px; border-top-color: $color-average-score; .card-score-big { - background-color: $orange-25; + background-color: $orange-50; } } @@ -99,7 +99,7 @@ $space-between-cards: 8px; border-top-color: $color-high-score; .card-score-big { - background-color: $green-25; + background-color: $green-50; } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 951580ea1fe..e4bd783c8bc 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -451,7 +451,7 @@ } .files { - margin-top: -1px; + margin-top: 1px; .diff-file:last-child { margin-bottom: 0; @@ -535,7 +535,6 @@ } .diff-notes-collapse { - position: relative; width: 19px; height: 19px; padding: 0; @@ -543,11 +542,7 @@ z-index: 100; svg { - position: absolute; - left: 50%; - top: 50%; - margin-left: -5.5px; - margin-top: -5.5px; + vertical-align: text-top; } path { @@ -586,11 +581,6 @@ top: 76px; } - + .files, - + .alert { - margin-top: 1px; - } - &:not(.is-stuck) .diff-stats-additions-deletions-collapsed { display: none; } @@ -605,11 +595,6 @@ .inline-parallel-buttons { display: none; } - - + .files, - + .alert { - margin-top: 32px; - } } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d01ee4b033c..7eb28354e6d 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -7,7 +7,7 @@ .is-confidential { color: $orange-600; - background-color: $orange-50; + background-color: $orange-100; border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; @@ -223,14 +223,14 @@ top: $new-navbar-height; bottom: 0; right: 0; - transition: width .3s; + transition: width $right-sidebar-transition-duration; background: $gray-light; z-index: 200; overflow: hidden; .issuable-sidebar { width: calc(100% + 100px); - height: calc(100% - #{$new-navbar-height}); + height: 100%; overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 35cefd449f1..dbf3e2b763c 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -255,7 +255,7 @@ $colors: ( &.saved { .editor { - border-top: solid 2px $green-200; + border-top: solid 2px $green-300; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 5d7c85b16ef..be4db597689 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -103,7 +103,7 @@ .confidential-issue-warning { color: $orange-600; - background-color: $orange-50; + background-color: $orange-100; border-radius: $border-radius-default $border-radius-default 0 0; border: 1px solid $border-gray-normal; border-bottom: none; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 052c005a2e8..46d31e41ada 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -727,6 +727,12 @@ ul.notes { border-bottom-left-radius: 0; } + .btn { + svg path { + fill: $gray-darkest; + } + } + .btn.discussion-create-issue-btn { margin-left: -4px; border-radius: 0; @@ -741,10 +747,6 @@ ul.notes { border: 0; } } - - .new-issue-for-discussion path { - fill: $gray-darkest; - } } } @@ -817,16 +819,6 @@ ul.notes { vertical-align: middle; } -.discussion-next-btn { - svg { - margin: 0; - - path { - fill: $gray-darkest; - } - } -} - // Merge request notes in diffs .diff-file { // Diff is inline diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 9d03a042aa3..086dd528579 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -644,20 +644,20 @@ button.mini-pipeline-graph-dropdown-toggle { // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - @include mini-pipeline-graph-color($green-50, $green-500, $green-600); + @include mini-pipeline-graph-color($green-100, $green-500, $green-600); } &.ci-status-icon-failed { - @include mini-pipeline-graph-color($red-50, $red-500, $red-600); + @include mini-pipeline-graph-color($red-100, $red-500, $red-600); } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - @include mini-pipeline-graph-color($orange-50, $orange-500, $orange-600); + @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600); } &.ci-status-icon-running { - @include mini-pipeline-graph-color($blue-50, $blue-400, $blue-600); + @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600); } &.ci-status-icon-canceled, diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index c5d6ff66dd6..67abe6e88ed 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -291,7 +291,7 @@ table.u2f-registrations { .bordered-box { border: 1px solid $blue-300; border-radius: $border-radius-default; - background-color: $blue-25; + background-color: $blue-50; position: relative; display: flex; justify-content: center; @@ -379,7 +379,7 @@ table.u2f-registrations { .nav-wip { border: 1px solid $blue-500; - background: $blue-25; + background: $blue-50; padding: $gl-padding; margin-bottom: $gl-padding; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6400b72742c..1f7b6703909 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -516,7 +516,7 @@ a.deploy-project-label { text-align: center; width: 169px; - &:hover, + &:hover:not(.disabled), &.forked { background-color: $row-hover; border-color: $row-hover-border; @@ -543,6 +543,15 @@ a.deploy-project-label { padding-top: $gl-padding; color: $gl-text-color; + &.disabled { + opacity: .3; + cursor: not-allowed; + + &:hover { + text-decoration: none; + } + } + .caption { min-height: 30px; padding: $gl-padding 0; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 4d4d92f9494..c36fe25f74d 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -54,6 +54,10 @@ border-radius: $border-radius-default; color: $almost-black; + .code.white pre .hll { + background-color: $well-light-border !important; + } + .tree-content-holder { display: flex; min-height: 300px; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 13dd7b5a780..89ebe3f9917 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -28,9 +28,7 @@ input[type="checkbox"]:hover { } .search { - margin-right: 10px; - margin-left: 10px; - margin-top: ($header-height - 35) / 2; + margin: 4px 8px 0; form { @extend .form-control; @@ -38,15 +36,23 @@ input[type="checkbox"]:hover { padding: 4px; width: $search-input-width; line-height: 24px; + height: 32px; + border: 0; + border-radius: $border-radius-default; + transition: border-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration; &:hover { - border-color: lighten($dropdown-input-focus-border, 20%); - box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); + box-shadow: none; } } - .location-text { - font-style: normal; + .location-badge { + font-size: 12px; + margin: -4px 4px -4px -4px; + line-height: 25px; + padding: 4px 8px; + border-radius: $border-radius-default 0 0 $border-radius-default; + transition: border-color ease-in-out $default-transition-duration; } .search-input { @@ -56,59 +62,36 @@ input[type="checkbox"]:hover { margin-left: 5px; line-height: 25px; width: 98%; + color: $white-light; + background: none; + transition: color ease-in-out $default-transition-duration; } - .location-badge { - line-height: 25px; - padding: 0 5px; - border-radius: $border-radius-default; - font-size: 14px; - font-style: normal; - color: $note-disabled-comment-color; - display: inline-block; - background-color: $gray-normal; - vertical-align: top; - cursor: default; + .search-input::placeholder { + transition: color ease-in-out $default-transition-duration; } .search-input-container { - display: -webkit-flex; display: flex; position: relative; } .search-input-wrap { - // Fallback if flexbox is not supported - display: inline-block; - } - - .search-input-wrap { - width: 100%; - .search-icon, .clear-icon { position: absolute; right: 5px; - top: 0; - color: $location-icon-color; - - &::before { - font-family: FontAwesome; - font-weight: $gl-font-weight-normal; - font-style: normal; - } + top: 4px; } .search-icon { - @extend .fa-search; - transition: color 0.15s; + transition: color $default-transition-duration; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } .clear-icon { - @extend .fa-times; display: none; } @@ -148,21 +131,32 @@ input[type="checkbox"]:hover { form { @extend .form-control:focus; border-color: $dropdown-input-focus-border; - box-shadow: 0 0 4px $search-input-focus-shadow-color; - } + box-shadow: none; + + .search-input-wrap { + .search-icon, + .clear-icon { + color: $gl-text-color-tertiary; + transition: color ease-in-out $default-transition-duration; + } + } - .location-badge { - transition: all 0.15s; - background-color: $location-badge-active-bg; - color: $white-light; - } + .search-input { + color: $gl-text-color; + transition: color ease-in-out $default-transition-duration; + } - .search-input-wrap { - i { - color: $layout-link-gray; + .search-input::placeholder { + color: $gl-text-color-tertiary; } } + .location-badge { + transition: all $default-transition-duration; + background-color: $nav-badge-bg; + border-color: $border-color; + } + .dropdown-menu { transition-duration: 100ms, 75ms; transition-delay: 75ms, 100ms; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 36f622db136..25c80e1f950 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -18,7 +18,7 @@ } &.ci-failed { - @include status-color($red-50, $red-500, $red-600); + @include status-color($red-100, $red-500, $red-600); } &.ci-success { @@ -39,12 +39,12 @@ &.ci-pending, &.ci-failed_with_warnings, &.ci-success_with_warnings { - @include status-color($orange-50, $orange-500, $orange-700); + @include status-color($orange-100, $orange-500, $orange-700); } &.ci-info, &.ci-running { - @include status-color($blue-50, $blue-500, $blue-600); + @include status-color($blue-100, $blue-500, $blue-600); } &.ci-created, diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss index 7d9f3da79c5..06733b7f1a9 100644 --- a/app/assets/stylesheets/test.scss +++ b/app/assets/stylesheets/test.scss @@ -15,3 +15,9 @@ -ms-animation: none !important; animation: none !important; } + +// Disable sticky changes bar for tests +.diff-files-changed { + position: relative !important; + top: 0 !important; +} diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 16590e66d61..fb6d8c0bb81 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -22,8 +22,7 @@ class Admin::ApplicationsController < Admin::ApplicationController @application = Doorkeeper::Application.new(application_params) if @application.save - flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - redirect_to admin_application_url(@application) + redirect_to_admin_page else render :new end @@ -42,6 +41,13 @@ class Admin::ApplicationsController < Admin::ApplicationController redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.' end + protected + + def redirect_to_admin_page + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) + redirect_to admin_application_url(@application) + end + private def set_application diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index cbcef70e957..676a7203c7d 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -128,7 +128,7 @@ class Admin::UsersController < Admin::ApplicationController end respond_to do |format| - result = Users::UpdateService.new(user, user_params_with_pass).execute do |user| + result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user| user.skip_reconfirmation! end @@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController def remove_email email = user.emails.find(params[:email_id]) - success = Emails::DestroyService.new(user, email: email.email).execute + success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute respond_to do |format| if success @@ -219,7 +219,7 @@ class Admin::UsersController < Admin::ApplicationController end def update_user(&block) - result = Users::UpdateService.new(user).execute(&block) + result = Users::UpdateService.new(current_user, user: user).execute(&block) result[:status] == :success end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97922e39ba8..771c6f3034a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,6 +25,8 @@ class ApplicationController < ActionController::Base around_action :set_locale + after_action :set_page_title_header, if: -> { request.format == :json } + protect_from_forgery with: :exception helper_method :can?, :current_application_settings @@ -335,4 +337,9 @@ class ApplicationController < ActionController::Base sign_in user, store: false end end + + def set_page_title_header + # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8 + response.headers['Page-Title'] = page_title('GitLab').encode('ISO-8859-1') + end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 0d0e53d4b76..3181f517087 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -106,7 +106,7 @@ module IssuableCollections # @filter_params[:authorized_only] = true end - @filter_params + @filter_params.permit(IssuableFinder::VALID_PARAMS) end def set_default_state @@ -117,19 +117,32 @@ module IssuableCollections key = 'issuable_sort' cookies[key] = params[:sort] if params[:sort].present? - - # id_desc and id_asc are old values for these two. - cookies[key] = sort_value_recently_created if cookies[key] == 'id_desc' - cookies[key] = sort_value_oldest_created if cookies[key] == 'id_asc' - + cookies[key] = update_cookie_value(cookies[key]) params[:sort] = cookies[key] end def default_sort_order case params[:state] - when 'opened', 'all' then sort_value_recently_created + when 'opened', 'all' then sort_value_created_date when 'merged', 'closed' then sort_value_recently_updated - else sort_value_recently_created + else sort_value_created_date + end + end + + # Update old values to the actual ones. + def update_cookie_value(value) + case value + when 'id_asc' then sort_value_oldest_created + when 'id_desc' then sort_value_recently_created + when 'created_asc' then sort_value_created_date + when 'created_desc' then sort_value_created_date + when 'due_date_asc' then sort_value_due_date + when 'due_date_desc' then sort_value_due_date + when 'milestone_due_asc' then sort_value_milestone + when 'milestone_due_desc' then sort_value_milestone + when 'downvotes_asc' then sort_value_popularity + when 'downvotes_desc' then sort_value_popularity + else value end end end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 306afb65f10..10d2665c06a 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -12,10 +12,14 @@ class ConfirmationsController < Devise::ConfirmationsController def after_confirmation_path_for(resource_name, resource) if signed_in?(resource_name) - after_sign_in_path_for(resource) + after_sign_in(resource) else flash[:notice] += " Please sign in." new_session_path(resource_name) end end + + def after_sign_in(resource) + after_sign_in_path_for(resource) + end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 87c0f8905ff..572915a4930 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -3,8 +3,13 @@ class HelpController < ApplicationController layout 'help' + # Taken from Jekyll + # https://github.com/jekyll/jekyll/blob/3.5-stable/lib/jekyll/document.rb#L13 + YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m + def index - @help_index = File.read(Rails.root.join('doc', 'README.md')) + # Remove YAML frontmatter so that it doesn't look weird + @help_index = File.read(Rails.root.join('doc', 'README.md')).sub(YAML_FRONT_MATTER_REGEXP, '') # Prefix Markdown links with `help/` unless they are external links # See http://rubular.com/r/X3baHTbPO2 @@ -22,7 +27,8 @@ class HelpController < ApplicationController path = File.join(Rails.root, 'doc', "#{@path}.md") if File.exist?(path) - @markdown = File.read(path) + # Remove YAML frontmatter so that it doesn't look weird + @markdown = File.read(path).gsub(YAML_FRONT_MATTER_REGEXP, '') render 'show.html.haml' else diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 2ae4785b12c..b02e64a132b 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -21,14 +21,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController @application.owner = current_user if @application.save - flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - redirect_to oauth_application_url(@application) + redirect_to_oauth_application_page else set_index_vars render :index end end + protected + + def redirect_to_oauth_application_page + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) + redirect_to oauth_application_url(@application) + end + private def verify_user_oauth_applications_enabled diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb index 408650aac54..39b9f8a84d1 100644 --- a/app/controllers/profiles/avatars_controller.rb +++ b/app/controllers/profiles/avatars_controller.rb @@ -2,7 +2,7 @@ class Profiles::AvatarsController < Profiles::ApplicationController def destroy @user = current_user - Users::UpdateService.new(@user).execute { |user| user.remove_avatar! } + Users::UpdateService.new(current_user, user: @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 ddb67d1c4d1..97db84b92d4 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -5,7 +5,7 @@ class Profiles::EmailsController < Profiles::ApplicationController end def create - @email = Emails::CreateService.new(current_user, email_params).execute + @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute if @email.errors.blank? NotificationService.new.new_email(@email) @@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController def destroy @email = current_user.emails.find(params[:id]) - Emails::DestroyService.new(current_user, email: @email.email).execute + Emails::DestroyService.new(current_user, user: 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/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 89d6d7f1b52..069e6a810f2 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -14,7 +14,7 @@ class Profiles::KeysController < Profiles::ApplicationController @key = Keys::CreateService.new(current_user, key_params).execute if @key.persisted? - redirect_to profile_key_path(@key) + redirect_to_profile_key_path else @keys = current_user.keys.select(&:persisted?) render :index @@ -50,6 +50,12 @@ class Profiles::KeysController < Profiles::ApplicationController end end + protected + + def redirect_to_profile_key_path + redirect_to profile_key_path(@key) + end + private def key_params diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 960b7512602..8a38ba65d4c 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -7,7 +7,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def update - result = Users::UpdateService.new(current_user, user_params).execute + result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute if result[:status] == :success flash[:notice] = "Notification settings saved" diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index 7beb52dd8e8..dcfcb855ab5 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -21,10 +21,10 @@ class Profiles::PasswordsController < Profiles::ApplicationController password_automatically_set: false } - result = Users::UpdateService.new(@user, password_attributes).execute + result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute if result[:status] == :success - Users::UpdateService.new(@user, password_expires_at: nil).execute + Users::UpdateService.new(current_user, user: @user, password_expires_at: nil).execute redirect_to root_path, notice: 'Password successfully changed' else @@ -46,7 +46,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController return end - result = Users::UpdateService.new(@user, password_attributes).execute + result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute if result[:status] == :success flash[:notice] = "Password was successfully updated. Please login with it" diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index cce2a847b53..ed0f98179eb 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -6,7 +6,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController def update begin - result = Users::UpdateService.new(user, preferences_params).execute + result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute if result[:status] == :success flash[:notice] = 'Preferences saved.' diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 1a4f77639e7..aa9789f8a0f 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 - Users::UpdateService.new(current_user).execute! + Users::UpdateService.new(current_user, user: current_user).execute! if two_factor_authentication_required? && !current_user.two_factor_enabled? two_factor_authentication_reason( @@ -41,7 +41,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def create if current_user.validate_and_consume_otp!(params[:pin_code]) - Users::UpdateService.new(current_user, otp_required_for_login: true).execute! do |user| + Users::UpdateService.new(current_user, user: current_user, otp_required_for_login: true).execute! do |user| @codes = user.generate_otp_backup_codes! end @@ -70,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end def codes - Users::UpdateService.new(current_user).execute! do |user| + Users::UpdateService.new(current_user, user: current_user).execute! do |user| @codes = user.generate_otp_backup_codes! end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index d83824fef06..5d87037f012 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -10,7 +10,7 @@ class ProfilesController < Profiles::ApplicationController def update respond_to do |format| - result = Users::UpdateService.new(@user, user_params).execute + result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute if result[:status] == :success message = "Profile was successfully updated" @@ -25,7 +25,7 @@ class ProfilesController < Profiles::ApplicationController end def reset_private_token - Users::UpdateService.new(@user).execute! do |user| + Users::UpdateService.new(current_user, user: @user).execute! do |user| user.reset_authentication_token! end @@ -35,7 +35,7 @@ class ProfilesController < Profiles::ApplicationController end def reset_incoming_email_token - Users::UpdateService.new(@user).execute! do |user| + Users::UpdateService.new(current_user, user: @user).execute! do |user| user.reset_incoming_email_token! end @@ -45,7 +45,7 @@ class ProfilesController < Profiles::ApplicationController end def reset_rss_token - Users::UpdateService.new(@user).execute! do |user| + Users::UpdateService.new(current_user, user: @user).execute! do |user| user.reset_rss_token! end @@ -61,7 +61,7 @@ class ProfilesController < Profiles::ApplicationController end def update_username - result = Users::UpdateService.new(@user, username: user_params[:username]).execute + result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute options = if result[:status] == :success { notice: "Username successfully changed" } diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 2b8f3977e6e..183a6f88a6a 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -41,6 +41,8 @@ class Projects::BlobController < Projects::ApplicationController end format.json do + page_title @blob.path, @ref, @project.name_with_namespace + show_json end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 747768eefb1..a9cce578366 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,10 +15,14 @@ class Projects::BranchesController < Projects::ApplicationController respond_to do |format| format.html do @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 + Gitlab::GitalyClient.allow_n_plus_1_calls do + @max_commits = @branches.reduce(0) do |memo, branch| + diverging_commit_counts = repository.diverging_commit_counts(branch) + [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + end - @max_commits = @branches.reduce(0) do |memo, branch| - diverging_commit_counts = repository.diverging_commit_counts(branch) - [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + render end end format.json do diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 1a775def506..a62f05db7db 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -20,7 +20,12 @@ class Projects::CommitController < Projects::ApplicationController apply_diff_view_cookie! respond_to do |format| - format.html + format.html do + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599 + Gitlab::GitalyClient.allow_n_plus_1_calls do + render + end + end format.diff { render text: @commit.to_diff } format.patch { render text: @commit.to_patch } end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 3c8eaa24080..3cb4eb23981 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -17,6 +17,10 @@ class Projects::CompareController < Projects::ApplicationController def show apply_diff_view_cookie! + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430 + Gitlab::GitalyClient.allow_n_plus_1_calls do + render + end end def diff_for_path diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 3f83bef2c79..68978f8fdd1 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -9,14 +9,12 @@ class Projects::ForksController < Projects::ApplicationController def index base_query = project.forks.includes(:creator) - @forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute) + forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute @total_forks_count = base_query.size - @private_forks_count = @total_forks_count - @forks.size + @private_forks_count = @total_forks_count - forks.size @public_forks_count = @total_forks_count - @private_forks_count - @sort = params[:sort] || 'id_desc' - @forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present? - @forks = @forks.order_by(@sort).page(params[:page]) + @forks = forks.page(params[:page]) respond_to do |format| format.html diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 42bfa4b9d4f..a3ec79a56d9 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -71,9 +71,6 @@ class Projects::IssuesController < Projects::ApplicationController @noteable = @issue @note = @project.notes.new(noteable: @issue) - @discussions = @issue.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable) - respond_to do |format| format.html format.json do diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index d60a24d3f1d..7d16e77ef66 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -10,7 +10,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic def show @environment = @merge_request.environments_for(current_user).last - render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431 + Gitlab::GitalyClient.allow_n_plus_1_calls do + render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } + end end def diff_for_path diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3aa5dadb5ca..c5204080333 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -56,6 +56,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo close_merge_request_without_source_project check_if_can_be_merged + # Return if the response has already been rendered + return if response_body + respond_to do |format| format.html do # Build a note object for comment form @@ -70,6 +73,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo labels set_pipeline_variables + + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37432 + Gitlab::GitalyClient.allow_n_plus_1_calls do + render + end end format.json do diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index dfa5e4f7f46..fb68dd771a1 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -8,19 +8,24 @@ class Projects::NetworkController < Projects::ApplicationController before_action :assign_commit def show - @url = project_network_path(@project, @ref, @options.merge(format: :json)) - @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s") + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602 + Gitlab::GitalyClient.allow_n_plus_1_calls do + @url = project_network_path(@project, @ref, @options.merge(format: :json)) + @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s") - respond_to do |format| - format.html do - if @options[:extended_sha1] && !@commit - flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist." + respond_to do |format| + format.html do + if @options[:extended_sha1] && !@commit + flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist." + end end - end - format.json do - @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref]) + format.json do + @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref]) + end end + + render end end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 1eb78d8b522..2fd015df688 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -51,13 +51,16 @@ class Projects::RefsController < Projects::ApplicationController contents.push(*tree.blobs) contents.push(*tree.submodules) - @logs = contents[@offset, @limit].to_a.map do |content| - file = @path ? File.join(@path, content.name) : content.name - last_commit = @repo.last_commit_for_path(@commit.id, file) - { - file_name: content.name, - commit: last_commit - } + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433 + @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do + contents[@offset, @limit].to_a.map do |content| + file = @path ? File.join(@path, content.name) : content.name + last_commit = @repo.last_commit_for_path(@commit.id, file) + { + file_name: content.name, + commit: last_commit + } + end end offset = (@offset + @limit) diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 1fc276b8c03..f3719059f88 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -35,7 +35,12 @@ class Projects::TreeController < Projects::ApplicationController end format.json do - render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree) + page_title @path.presence || _("Files"), @ref, @project.name_with_namespace + + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261 + Gitlab::GitalyClient.allow_n_plus_1_calls do + render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree) + end end end end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 6966a7c5fee..4d2fb17a19b 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -28,7 +28,7 @@ class Projects::UploadsController < Projects::ApplicationController end def image_or_video? - uploader && uploader.file.exists? && uploader.image_or_video? + uploader && uploader.exists? && uploader.image_or_video? end def uploader_class diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 1b4545e4a49..19e38993038 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -13,7 +13,10 @@ class RootController < Dashboard::ProjectsController before_action :redirect_logged_user, if: -> { current_user.present? } def index - super + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434 + Gitlab::GitalyClient.allow_n_plus_1_calls do + super + end end private diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index be6491d042c..fe3bb117410 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -55,7 +55,7 @@ class SessionsController < Devise::SessionsController return unless user && user.require_password_creation? - Users::UpdateService.new(user).execute do |user| + Users::UpdateService.new(current_user, user: user).execute do |user| @token = user.generate_reset_token end diff --git a/app/finders/concerns/custom_attributes_filter.rb b/app/finders/concerns/custom_attributes_filter.rb new file mode 100644 index 00000000000..5bbf9ca242d --- /dev/null +++ b/app/finders/concerns/custom_attributes_filter.rb @@ -0,0 +1,20 @@ +module CustomAttributesFilter + def by_custom_attributes(items) + return items unless params[:custom_attributes].is_a?(Hash) + return items unless Ability.allowed?(current_user, :read_custom_attribute) + + association = items.reflect_on_association(:custom_attributes) + attributes_table = association.klass.arel_table + attributable_table = items.model.arel_table + + custom_attributes = association.klass.select('true').where( + attributes_table[association.foreign_key] + .eq(attributable_table[association.association_primary_key]) + ) + + # perform a subquery for each attribute to be filtered + params[:custom_attributes].inject(items) do |scope, (key, value)| + scope.where('EXISTS (?)', custom_attributes.where(key: key, value: value)) + end + end +end diff --git a/app/finders/fork_projects_finder.rb b/app/finders/fork_projects_finder.rb new file mode 100644 index 00000000000..28d1b31868e --- /dev/null +++ b/app/finders/fork_projects_finder.rb @@ -0,0 +1,6 @@ +class ForkProjectsFinder < ProjectsFinder + def initialize(project, params: {}, current_user: nil) + project_ids = project.forks.includes(:creator).select(:id) + super(params: params, current_user: current_user, project_ids_relation: project_ids) + end +end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 88d71b0a87b..0c4c4b10fb6 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -57,7 +57,7 @@ class GroupsFinder < UnionFinder end def owned_groups - current_user&.groups || Group.none + current_user&.owned_groups || Group.none end def include_public_groups? diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 0a2e3c709d9..24c07f3dc70 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -25,6 +25,28 @@ class IssuableFinder NONE = '0'.freeze + SCALAR_PARAMS = %i[ + assignee_id + assignee_username + author_id + author_username + authorized_only + due_date + group_id + iids + label_name + milestone_title + non_archived + project_id + scope + search + sort + state + ].freeze + ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze + + VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze + attr_accessor :current_user, :params def initialize(current_user, params = {}) diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 33f7ae90598..1a7e97004fb 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -15,6 +15,7 @@ # class UsersFinder include CreatedAtFilter + include CustomAttributesFilter attr_accessor :current_user, :params @@ -32,6 +33,7 @@ class UsersFinder users = by_external_identity(users) users = by_external(users) users = by_created_at(users) + users = by_custom_attributes(users) users end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index cdf5fa5d4b7..8ad94d3f723 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -30,10 +30,4 @@ module AppearancesHelper render 'shared/logo.svg' end end - - def custom_icon(icon_name, size: 16) - # We can't simply do the below, because there are some .erb SVGs. - # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe - render "shared/icons/#{icon_name}.svg", size: size - end end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index c455d18cff8..483b957decb 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -5,6 +5,25 @@ module AutoDevopsHelper can?(current_user, :admin_pipeline, project) && project.has_auto_devops_implicitly_disabled? && !project.repository.gitlab_ci_yml && - project.ci_services.active.none? + !project.ci_service + end + + def auto_devops_warning_message(project) + missing_domain = !project.auto_devops&.has_domain? + missing_service = !project.kubernetes_service&.active? + + if missing_service + params = { + kubernetes: link_to('Kubernetes service', edit_project_service_path(project, 'kubernetes')) + } + + if missing_domain + _('Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly.') % params + else + _('Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly.') % params + end + elsif missing_domain + _('Auto Review Apps and Auto Deploy need a domain name to work correctly.') + end end end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index a4c226a6aad..be11d453898 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -13,22 +13,29 @@ module AvatarsHelper user_name = options[:user].try(:name) || options[:user_name] avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip] - data_attributes = {} + data_attributes = options[:data] || {} css_class = %W[avatar s#{avatar_size}].push(*options[:css_class]) if has_tooltip css_class.push('has-tooltip') - data_attributes = { container: 'body' } + data_attributes[:container] = 'body' end - image_tag( - avatar_url, + if options[:lazy] + css_class << 'lazy' + data_attributes[:src] = avatar_url + avatar_url = LazyImageTagHelper.placeholder_image + end + + image_options = { + alt: "#{user_name}'s avatar", + src: avatar_url, + data: data_attributes, class: css_class, - alt: "#{user_name}'s avatar", - title: user_name, - data: data_attributes, - lazy: true - ) + title: user_name + } + + tag(:img, image_options) end def user_avatar(options = {}) diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 62ac208f16a..7112c6ee470 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -79,6 +79,6 @@ module BoardsHelper end def boards_link_text - _("Board") + s_("IssueBoards|Board") end end diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb index ee1b7ed083e..e88fe6bcd7e 100644 --- a/app/helpers/breadcrumbs_helper.rb +++ b/app/helpers/breadcrumbs_helper.rb @@ -10,11 +10,7 @@ module BreadcrumbsHelper def breadcrumb_title_link return @breadcrumb_link if @breadcrumb_link - if controller.available_action?(:index) - url_for(action: "index") - else - request.path - end + request.path end def breadcrumb_title(title) @@ -25,7 +21,7 @@ module BreadcrumbsHelper def breadcrumb_list_item(link) content_tag "li" do - link + icon("angle-right", class: "breadcrumbs-list-angle") + link + sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle") end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 08fb9db6c0f..ef22cafc2e2 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -176,13 +176,15 @@ module CommitsHelper end end - def view_file_button(commit_sha, diff_new_path, project) + def view_file_button(commit_sha, diff_new_path, project, replaced: false) + title = replaced ? _('View replaced file @ ') : _('View file @ ') + link_to( project_blob_path(project, tree_join(commit_sha, diff_new_path)), class: 'btn view-file js-view-file' ) do - raw('View file @ ') + content_tag(:span, Commit.truncate_sha(commit_sha), + raw(title) + content_tag(:span, Commit.truncate_sha(commit_sha), class: 'commit-sha') end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index e8efe8fab27..82bceddf1f0 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -125,7 +125,7 @@ module GroupsHelper end def default_help - s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.") + s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.") end def ancestor_locked_but_you_can_override(group) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 9a404832423..ec779c1c447 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -17,6 +17,18 @@ module IconsHelper options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) end + def custom_icon(icon_name, size: 16) + # We can't simply do the below, because there are some .erb SVGs. + # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe + render "shared/icons/#{icon_name}.svg", size: size + end + + def sprite_icon(icon_name, size: nil, css_class: nil) + css_classes = size ? "s#{size}" : "" + css_classes << " #{css_class}" unless css_class.blank? + content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) + end + def audit_icon(names, options = {}) case names when "standard" diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index df390dd5aab..7713fb0b9f8 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -248,16 +248,25 @@ module IssuablesHelper Gitlab::IssuablesCountForState.new(finder)[state] end - def close_issuable_url(issuable) - issuable_url(issuable, close_reopen_params(issuable, :close)) + def close_issuable_path(issuable) + issuable_path(issuable, close_reopen_params(issuable, :close)) end - def reopen_issuable_url(issuable) - issuable_url(issuable, close_reopen_params(issuable, :reopen)) + def reopen_issuable_path(issuable) + issuable_path(issuable, close_reopen_params(issuable, :reopen)) end - def close_reopen_issuable_url(issuable, should_inverse = false) - issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable) + def close_reopen_issuable_path(issuable, should_inverse = false) + issuable.closed? ^ should_inverse ? reopen_issuable_path(issuable) : close_issuable_path(issuable) + end + + def issuable_path(issuable, *options) + case issuable + when Issue + issue_path(issuable, *options) + when MergeRequest + merge_request_path(issuable, *options) + end end def issuable_url(issuable, *options) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 446a59030a6..be8cb358de2 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -94,6 +94,12 @@ module MilestonesHelper end end + def milestone_tooltip_title(milestone) + if milestone.due_date + [milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ') + end + end + def milestone_remaining_days(milestone) if milestone.expired? content_tag(:strong, 'Past due') diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 5946c475835..18b9bf214a3 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -9,7 +9,7 @@ module PageLayoutHelper end # Segments are seperated by middot - @page_title.join(" \u00b7 ") + @page_title.join(" · ") end # Define or get a description for the current page diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ddeff490d3a..21fb17e06d6 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -239,8 +239,8 @@ module ProjectsHelper end end - def has_projects_or_name?(projects, params) - !!(params[:name] || any_projects?(projects)) + def show_projects?(projects, params) + !!(params[:personal] || params[:name] || any_projects?(projects)) end private diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index b408ec0c6a4..1b542ed2a96 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -1,34 +1,38 @@ module SortingHelper def sort_options_hash { - sort_value_name => sort_title_name, - sort_value_name_desc => sort_title_name_desc, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_created_date => sort_title_created_date, + sort_value_downvotes => sort_title_downvotes, + sort_value_due_date => sort_title_due_date, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_label_priority => sort_title_label_priority, + sort_value_largest_group => sort_title_largest_group, + sort_value_largest_repo => sort_title_largest_repo, + sort_value_milestone => sort_title_milestone, + sort_value_milestone_later => sort_title_milestone_later, + sort_value_milestone_soon => sort_title_milestone_soon, + sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created, - sort_value_milestone_soon => sort_title_milestone_soon, - sort_value_milestone_later => sort_title_milestone_later, - sort_value_due_date_soon => sort_title_due_date_soon, - sort_value_due_date_later => sort_title_due_date_later, - sort_value_largest_repo => sort_title_largest_repo, - sort_value_largest_group => sort_title_largest_group, - sort_value_recently_signin => sort_title_recently_signin, - sort_value_oldest_signin => sort_title_oldest_signin, - sort_value_downvotes => sort_title_downvotes, - sort_value_upvotes => sort_title_upvotes, - sort_value_priority => sort_title_priority, - sort_value_label_priority => sort_title_label_priority + sort_value_recently_signin => sort_title_recently_signin, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_popularity => sort_title_popularity, + sort_value_priority => sort_title_priority, + sort_value_upvotes => sort_title_upvotes } end def projects_sort_options_hash options = { - sort_value_name => sort_title_name, - sort_value_latest_activity => sort_title_latest_activity, - sort_value_oldest_activity => sort_title_oldest_activity, - sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created + sort_value_latest_activity => sort_title_latest_activity, + sort_value_name => sort_title_name, + sort_value_oldest_activity => sort_title_oldest_activity, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_recently_created => sort_title_recently_created } if current_controller?('admin/projects') @@ -40,160 +44,174 @@ module SortingHelper def member_sort_options_hash { - sort_value_access_level_asc => sort_title_access_level_asc, + sort_value_access_level_asc => sort_title_access_level_asc, sort_value_access_level_desc => sort_title_access_level_desc, - sort_value_last_joined => sort_title_last_joined, - sort_value_oldest_joined => sort_title_oldest_joined, - sort_value_name => sort_title_name_asc, - sort_value_name_desc => sort_title_name_desc, - sort_value_recently_signin => sort_title_recently_signin, - sort_value_oldest_signin => sort_title_oldest_signin + sort_value_last_joined => sort_title_last_joined, + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_oldest_joined => sort_title_oldest_joined, + sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_recently_signin => sort_title_recently_signin } end def milestone_sort_options_hash { - sort_value_name => sort_title_name_asc, - sort_value_name_desc => sort_title_name_desc, - sort_value_due_date_soon => sort_title_due_date_soon, - sort_value_due_date_later => sort_title_due_date_later, - sort_value_start_date_soon => sort_title_start_date_soon, - sort_value_start_date_later => sort_title_start_date_later + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_start_date_later => sort_title_start_date_later, + sort_value_start_date_soon => sort_title_start_date_soon } end def branches_sort_options_hash { - sort_value_name => sort_title_name, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated + sort_value_name => sort_title_name, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_updated => sort_title_recently_updated } end def tags_sort_options_hash { - sort_value_name => sort_title_name, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated + sort_value_name => sort_title_name, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_updated => sort_title_recently_updated } end - def sort_title_priority - 'Priority' + def sortable_item(item, path, sorted_by) + link_to item, path, class: sorted_by == item ? 'is-active' : '' end - def sort_title_label_priority - 'Label priority' + # Titles. + def sort_title_access_level_asc + s_('SortOptions|Access level, ascending') end - def sort_title_oldest_updated - 'Oldest updated' + def sort_title_access_level_desc + s_('SortOptions|Access level, descending') end - def sort_title_recently_updated - 'Last updated' + def sort_title_created_date + s_('SortOptions|Created date') end - def sort_title_oldest_activity - 'Oldest updated' + def sort_title_downvotes + s_('SortOptions|Least popular') end - def sort_title_latest_activity - 'Last updated' + def sort_title_due_date + s_('SortOptions|Due date') end - def sort_title_oldest_created - 'Oldest created' + def sort_title_due_date_later + s_('SortOptions|Due later') end - def sort_title_recently_created - 'Last created' + def sort_title_due_date_soon + s_('SortOptions|Due soon') end - def sort_title_milestone_soon - 'Milestone due soon' + def sort_title_label_priority + s_('SortOptions|Label priority') end - def sort_title_milestone_later - 'Milestone due later' + def sort_title_largest_group + s_('SortOptions|Largest group') end - def sort_title_due_date_soon - 'Due soon' + def sort_title_largest_repo + s_('SortOptions|Largest repository') end - def sort_title_due_date_later - 'Due later' + def sort_title_last_joined + s_('SortOptions|Last joined') end - def sort_title_start_date_soon - 'Start soon' + def sort_title_latest_activity + s_('SortOptions|Last updated') end - def sort_title_start_date_later - 'Start later' + def sort_title_milestone + s_('SortOptions|Milestone') + end + + def sort_title_milestone_later + s_('SortOptions|Milestone due later') + end + + def sort_title_milestone_soon + s_('SortOptions|Milestone due soon') end def sort_title_name - 'Name' + s_('SortOptions|Name') end - def sort_title_largest_repo - 'Largest repository' + def sort_title_name_asc + s_('SortOptions|Name, ascending') end - def sort_title_largest_group - 'Largest group' + def sort_title_name_desc + s_('SortOptions|Name, descending') end - def sort_title_recently_signin - 'Recent sign in' + def sort_title_oldest_activity + s_('SortOptions|Oldest updated') end - def sort_title_oldest_signin - 'Oldest sign in' + def sort_title_oldest_created + s_('SortOptions|Oldest created') end - def sort_title_downvotes - 'Least popular' + def sort_title_oldest_joined + s_('SortOptions|Oldest joined') end - def sort_title_upvotes - 'Most popular' + def sort_title_oldest_signin + s_('SortOptions|Oldest sign in') end - def sort_title_last_joined - 'Last joined' + def sort_title_oldest_updated + s_('SortOptions|Oldest updated') end - def sort_title_oldest_joined - 'Oldest joined' + def sort_title_popularity + s_('SortOptions|Popularity') end - def sort_title_access_level_asc - 'Access level, ascending' + def sort_title_priority + s_('SortOptions|Priority') end - def sort_title_access_level_desc - 'Access level, descending' + def sort_title_recently_created + s_('SortOptions|Last created') end - def sort_title_name_asc - 'Name, ascending' + def sort_title_recently_signin + s_('SortOptions|Recent sign in') end - def sort_title_name_desc - 'Name, descending' + def sort_title_recently_updated + s_('SortOptions|Last updated') end - def sort_value_last_joined - 'last_joined' + def sort_title_start_date_later + s_('SortOptions|Start later') end - def sort_value_oldest_joined - 'oldest_joined' + def sort_title_start_date_soon + s_('SortOptions|Start soon') + end + + def sort_title_upvotes + s_('SortOptions|Most popular') end + # Values. def sort_value_access_level_asc 'access_level_asc' end @@ -202,88 +220,112 @@ module SortingHelper 'access_level_desc' end - def sort_value_name_desc - 'name_desc' + def sort_value_created_date + 'created_date' end - def sort_value_priority - 'priority' + def sort_value_downvotes + 'downvotes_desc' + end + + def sort_value_due_date + 'due_date' + end + + def sort_value_due_date_later + 'due_date_desc' + end + + def sort_value_due_date_soon + 'due_date_asc' end def sort_value_label_priority 'label_priority' end - def sort_value_oldest_updated - 'updated_asc' + def sort_value_largest_group + 'storage_size_desc' end - def sort_value_recently_updated - 'updated_desc' + def sort_value_largest_repo + 'storage_size_desc' end - def sort_value_oldest_activity - 'latest_activity_asc' + def sort_value_last_joined + 'last_joined' end def sort_value_latest_activity 'latest_activity_desc' end - def sort_value_oldest_created - 'created_asc' + def sort_value_milestone + 'milestone' end - def sort_value_recently_created - 'created_desc' + def sort_value_milestone_later + 'milestone_due_desc' end def sort_value_milestone_soon 'milestone_due_asc' end - def sort_value_milestone_later - 'milestone_due_desc' + def sort_value_name + 'name_asc' end - def sort_value_due_date_soon - 'due_date_asc' + def sort_value_name_desc + 'name_desc' end - def sort_value_due_date_later - 'due_date_desc' + def sort_value_oldest_activity + 'latest_activity_asc' end - def sort_value_start_date_soon - 'start_date_asc' + def sort_value_oldest_created + 'created_asc' end - def sort_value_start_date_later - 'start_date_desc' + def sort_value_oldest_signin + 'oldest_sign_in' end - def sort_value_name - 'name_asc' + def sort_value_oldest_joined + 'oldest_joined' end - def sort_value_largest_repo - 'storage_size_desc' + def sort_value_oldest_updated + 'updated_asc' end - def sort_value_largest_group - 'storage_size_desc' + def sort_value_popularity + 'popularity' + end + + def sort_value_priority + 'priority' + end + + def sort_value_recently_created + 'created_desc' end def sort_value_recently_signin 'recent_sign_in' end - def sort_value_oldest_signin - 'oldest_sign_in' + def sort_value_recently_updated + 'updated_desc' end - def sort_value_downvotes - 'downvotes_desc' + def sort_value_start_date_later + 'start_date_desc' + end + + def sort_value_start_date_soon + 'start_date_asc' end def sort_value_upvotes diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index c98f65c7644..d7eaf6ce24d 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -1,25 +1,25 @@ module SystemNoteHelper ICON_NAMES_BY_ACTION = { - 'commit' => 'icon_commit', - 'description' => 'icon_edit', - 'merge' => 'icon_merge', - 'merged' => 'icon_merged', - 'opened' => 'icon_status_open', - 'closed' => 'icon_status_closed', - 'time_tracking' => 'icon_stopwatch', - 'assignee' => 'icon_user', - 'title' => 'icon_edit', - 'task' => 'icon_check_square_o', - 'label' => 'icon_tags', - 'cross_reference' => 'icon_random', - 'branch' => 'icon_code_fork', - 'confidential' => 'icon_eye_slash', - 'visible' => 'icon_eye', - 'milestone' => 'icon_clock_o', - 'discussion' => 'icon_comment_o', - 'moved' => 'icon_arrow_circle_o_right', - 'outdated' => 'icon_edit', - 'duplicate' => 'icon_clone' + 'commit' => 'commit', + 'description' => 'pencil', + 'merge' => 'git-merge', + 'merged' => 'git-merge', + 'opened' => 'issue-open', + 'closed' => 'issue-close', + 'time_tracking' => 'timer', + 'assignee' => 'user', + 'title' => 'pencil', + 'task' => 'task-done', + 'label' => 'label', + 'cross_reference' => 'comment-dots', + 'branch' => 'fork', + 'confidential' => 'eye-slash', + 'visible' => 'eye', + 'milestone' => 'clock', + 'discussion' => 'comment', + 'moved' => 'arrow-right', + 'outdated' => 'pencil', + 'duplicate' => 'issue-duplicate' }.freeze def system_note_icon_name(note) @@ -28,7 +28,7 @@ module SystemNoteHelper def icon_for_system_note(note) icon_name = system_note_icon_name(note) - custom_icon(icon_name) if icon_name + sprite_icon(icon_name) if icon_name end extend self diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 8d017b9b3b1..3d5acc00f8f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -31,6 +31,7 @@ module Ci has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' delegate :id, to: :project, prefix: true + delegate :full_path, to: :project, prefix: true validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create validates :sha, presence: { unless: :importing? } @@ -336,7 +337,7 @@ module Ci return @config_processor if defined?(@config_processor) @config_processor ||= begin - Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path) + Gitlab::Ci::YamlProcessor.new(ci_yaml_file) rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e self.yaml_errors = e.message nil @@ -433,7 +434,7 @@ module Ci def update_duration return unless started_at - self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self) + self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self) end def execute_hooks diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index a0d07902ba2..c6509f89117 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -174,7 +174,7 @@ module Ci end def assignable_for?(project) - !locked? || projects.exists?(id: project.id) + is_shared? || projects.exists?(id: project.id) end def accepting_tags?(build) diff --git a/app/models/commit.rb b/app/models/commit.rb index 2ae8890c1b3..6dba154a6ea 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -25,8 +25,8 @@ class Commit DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_LINES = 50000 - # The SHA can be between 7 and 40 hex characters. - COMMIT_SHA_PATTERN = '\h{7,40}'.freeze + MIN_SHA_LENGTH = 7 + COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze def banzai_render_context(field) context = { pipeline: :single_line, project: self.project } @@ -53,7 +53,7 @@ class Commit # Truncate sha to 8 characters def truncate_sha(sha) - sha[0..7] + sha[0..MIN_SHA_LENGTH] end def max_diff_options @@ -100,7 +100,7 @@ class Commit def self.reference_pattern @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? - (?<commit>\h{7,40}) + (?<commit>#{COMMIT_SHA_PATTERN}) }x end @@ -216,9 +216,8 @@ class Commit @raw.respond_to?(method, include_private) || super end - # Truncate sha to 8 characters def short_id - @raw.short_id(7) + @raw.short_id(MIN_SHA_LENGTH) end def diff_refs diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 265f6e48540..fc30d008dea 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -143,16 +143,18 @@ module Issuable end def sort(method, excluded_labels: []) - sorted = case method.to_s - when 'milestone_due_asc' then order_milestone_due_asc - when 'milestone_due_desc' then order_milestone_due_desc - when 'downvotes_desc' then order_downvotes_desc - when 'upvotes_desc' then order_upvotes_desc - when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) - when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) - else - order_by(method) - end + sorted = + case method.to_s + when 'downvotes_desc' then order_downvotes_desc + when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) + when 'milestone' then order_milestone_due_asc + when 'milestone_due_asc' then order_milestone_due_asc + when 'milestone_due_desc' then order_milestone_due_desc + when 'popularity' then order_upvotes_desc + when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) + when 'upvotes_desc' then order_upvotes_desc + else order_by(method) + end # Break ties with the ID column for pagination sorted.order(id: :desc) @@ -214,7 +216,7 @@ module Issuable def grouping_columns(sort) grouping_columns = [arel_table[:id]] - if %w(milestone_due_desc milestone_due_asc).include?(sort) + if %w(milestone_due_desc milestone_due_asc milestone).include?(sort) milestone_table = Milestone.arel_table grouping_columns << milestone_table[:id] grouping_columns << milestone_table[:due_date] diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index db3cd257584..cefa5c13c5f 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -19,14 +19,15 @@ module Sortable module ClassMethods def order_by(method) case method.to_s - when 'name_asc' then order_name_asc - when 'name_desc' then order_name_desc - when 'updated_asc' then order_updated_asc - when 'updated_desc' then order_updated_desc - when 'created_asc' then order_created_asc + when 'created_asc' then order_created_asc + when 'created_date' then order_created_desc when 'created_desc' then order_created_desc - when 'id_desc' then order_id_desc - when 'id_asc' then order_id_asc + when 'id_asc' then order_id_asc + when 'id_desc' then order_id_desc + when 'name_asc' then order_name_asc + when 'name_desc' then order_name_desc + when 'updated_asc' then order_updated_asc + when 'updated_desc' then order_updated_desc else all end diff --git a/app/models/environment.rb b/app/models/environment.rb index 44e39e21442..b6868ccbe8f 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -6,9 +6,7 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true, validate: true - has_many :deployments, - -> (env) { where(project_id: env.project_id) }, - dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' diff --git a/app/models/event.rb b/app/models/event.rb index 0b1f053a7e6..0997b056c6a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,7 +1,7 @@ class Event < ActiveRecord::Base include Sortable include IgnorableColumn - default_scope { reorder(nil).where.not(author_id: nil) } + default_scope { reorder(nil) } CREATED = 1 UPDATED = 2 @@ -77,6 +77,12 @@ class Event < ActiveRecord::Base scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } + # Authors are required as they're used to display who pushed data. + # + # We're just validating the presence of the ID here as foreign key constraints + # should ensure the ID points to a valid user. + validates :author_id, presence: true + self.inheritance_column = 'action' # "data" will be removed in 10.0 but it may be possible that JOINs happen that diff --git a/app/models/issue.rb b/app/models/issue.rb index cd5056aae5e..155c5d972b7 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -116,7 +116,8 @@ class Issue < ActiveRecord::Base def self.sort(method, excluded_labels: []) case method.to_s - when 'due_date_asc' then order_due_date_asc + when 'due_date' then order_due_date_asc + when 'due_date_asc' then order_due_date_asc when 'due_date_desc' then order_due_date_desc else super @@ -275,8 +276,6 @@ class Issue < ActiveRecord::Base end def update_project_counter_caches - return unless update_project_counter_caches? - Projects::OpenIssuesCountService.new(project).refresh_cache end diff --git a/app/models/key.rb b/app/models/key.rb index 4fa6cac2fd0..0c41e34d969 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -4,8 +4,6 @@ class Key < ActiveRecord::Base include Gitlab::CurrentSettings include Sortable - LAST_USED_AT_REFRESH_TIME = 1.day.to_i - belongs_to :user before_validation :generate_fingerprint @@ -54,10 +52,7 @@ class Key < ActiveRecord::Base end def update_last_used_at - lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME) - return unless lease.try_obtain - - UseKeyWorker.perform_async(id) + Keys::LastUsedService.new(self).execute end def add_to_shell diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2a56bab48a3..8d9a30397a9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -415,8 +415,11 @@ class MergeRequest < ActiveRecord::Base end def create_merge_request_diff - merge_request_diffs.create - reload_merge_request_diff + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435 + Gitlab::GitalyClient.allow_n_plus_1_calls do + merge_request_diffs.create + reload_merge_request_diff + end end def reload_merge_request_diff @@ -955,8 +958,6 @@ class MergeRequest < ActiveRecord::Base end def update_project_counter_caches - return unless update_project_counter_caches? - Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index a3070a12b7c..47e6b785c39 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -162,9 +162,7 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # - def to_reference(from_project = nil, format: :iid, full: false) - return if group_milestone? && format != :name - + def to_reference(from_project = nil, format: :name, full: false) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" @@ -241,6 +239,10 @@ class Milestone < ActiveRecord::Base def milestone_format_reference(format = :iid) raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) + if group_milestone? && format == :iid + raise ArgumentError, 'Cannot refer to a group milestone by an internal id!' + end + if format == :name && !name.include?('"') %("#{name}") else diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 3845e485413..aec7b01e23a 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -61,8 +61,11 @@ module Network @reserved[i] = [] end - commits_sort_by_ref.each do |commit| - place_chain(commit) + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37436 + Gitlab::GitalyClient.allow_n_plus_1_calls do + commits_sort_by_ref.each do |commit| + place_chain(commit) + end end # find parent spaces for not overlap lines diff --git a/app/models/project.rb b/app/models/project.rb index f7221e4f3b2..bb3f74c4b89 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -245,6 +245,9 @@ class Project < ActiveRecord::Base scope :pending_delete, -> { where(pending_delete: true) } scope :without_deleted, -> { where(pending_delete: false) } + scope :with_hashed_storage, -> { where('storage_version >= 1') } + scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) } + scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } @@ -1032,7 +1035,7 @@ class Project < ActiveRecord::Base # Forked import is handled asynchronously return if forked? && !force - if gitlab_shell.add_repository(repository_storage_path, disk_path) + if gitlab_shell.add_repository(repository_storage, disk_path) repository.after_create true else @@ -1550,18 +1553,44 @@ class Project < ActiveRecord::Base end def legacy_storage? - self.storage_version.nil? + [nil, 0].include?(self.storage_version) + end + + def hashed_storage? + self.storage_version && self.storage_version >= 1 end def renamed? persisted? && path_changed? end + def migrate_to_hashed_storage! + return if hashed_storage? + + update!(repository_read_only: true) + + if repo_reference_count > 0 || wiki_reference_count > 0 + ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id) + else + ProjectMigrateHashedStorageWorker.perform_async(id) + end + end + + def storage_version=(value) + super + + @storage = nil if storage_version_changed? + end + + def gl_repository(is_wiki:) + Gitlab::GlRepository.gl_repository(self, is_wiki) + end + private def storage @storage ||= - if self.storage_version && self.storage_version >= 1 + if hashed_storage? Storage::HashedProject.new(self) else Storage::LegacyProject.new(self) @@ -1574,6 +1603,14 @@ class Project < ActiveRecord::Base end end + def repo_reference_count + Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value + end + + def wiki_reference_count + Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value + end + # set last_activity_at to the same as created_at def set_last_activity_at update_column(:last_activity_at, self.created_at) diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 7af3b6870e2..9a52edbff8e 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -6,6 +6,10 @@ class ProjectAutoDevops < ActiveRecord::Base validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } + def has_domain? + domain.present? + end + def variables variables = [] variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present? diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 698fdf7a20c..c4cc1c1cf22 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -174,7 +174,7 @@ class ProjectWiki private def init_repo(disk_path) - gitlab_shell.add_repository(project.repository_storage_path, disk_path) + gitlab_shell.add_repository(project.repository_storage, disk_path) end def commit_details(action, message = nil, title = nil) diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 708513c7861..83ce9014094 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -3,12 +3,6 @@ class PushEvent < Event # different "action" value. validate :validate_push_action - # Authors are required as they're used to display who pushed data. - # - # We're just validating the presence of the ID here as foreign key constraints - # should ensure the ID points to a valid user. - validates :author_id, presence: true - # The project is required to build links to commits, commit ranges, etc. # # We're just validating the presence of the ID here as foreign key constraints diff --git a/app/models/repository.rb b/app/models/repository.rb index af9911ea045..1f4df50a913 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -8,6 +8,7 @@ class Repository RESERVED_REFS_NAMES = %W[ heads tags + replace #{REF_ENVIRONMENTS} #{REF_KEEP_AROUND} #{REF_ENVIRONMENTS} @@ -90,12 +91,6 @@ class Repository ) end - # we need to have this method here because it is not cached in ::Git and - # the method is called multiple times for every request - def has_visible_content? - branch_count > 0 - end - def inspect "#<#{self.class.name}:#{@disk_path}>" end @@ -488,13 +483,7 @@ class Repository def exists? return false unless full_path - Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| - if enabled - raw_repository.exists? - else - refs_directory_exists? - end - end + raw_repository.exists? end cache_method :exists? @@ -528,13 +517,17 @@ class Repository delegate :tag_names, to: :raw_repository cache_method :tag_names, fallback: [] - delegate :branch_count, :tag_count, to: :raw_repository + delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository cache_method :branch_count, fallback: 0 cache_method :tag_count, fallback: 0 + cache_method :has_visible_content?, fallback: false def avatar - if tree = file_on_head(:avatar) - tree.path + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38327 + Gitlab::GitalyClient.allow_n_plus_1_calls do + if tree = file_on_head(:avatar) + tree.path + end end end cache_method :avatar @@ -834,10 +827,6 @@ class Repository } end - def user_to_committer(user) - Gitlab::Git.committer_hash(email: user.email, name: user.name) - end - def can_be_merged?(source_sha, target_branch) our_commit = rugged.branches[target_branch].target their_commit = rugged.lookup(source_sha) @@ -859,54 +848,34 @@ class Repository end def revert( - user, commit, branch_name, + user, commit, branch_name, message, start_branch_name: nil, start_project: project) - with_branch( - user, - branch_name, - start_branch_name: start_branch_name, - start_repository: start_project.repository.raw_repository) do |start_commit| - - revert_tree_id = check_revert_content(commit, start_commit.sha) - unless revert_tree_id - raise Repository::CreateTreeError.new('Failed to revert commit') - end - committer = user_to_committer(user) - - create_commit(message: commit.revert_message(user), - author: committer, - committer: committer, - tree: revert_tree_id, - parents: [start_commit.sha]) + with_cache_hooks do + raw_repository.revert( + user: user, + commit: commit.raw, + branch_name: branch_name, + message: message, + start_branch_name: start_branch_name, + start_repository: start_project.repository.raw_repository + ) end end def cherry_pick( - user, commit, branch_name, + user, commit, branch_name, message, start_branch_name: nil, start_project: project) - with_branch( - user, - branch_name, - start_branch_name: start_branch_name, - start_repository: start_project.repository.raw_repository) do |start_commit| - cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) - unless cherry_pick_tree_id - raise Repository::CreateTreeError.new('Failed to cherry-pick commit') - end - - committer = user_to_committer(user) - - create_commit(message: commit.cherry_pick_message(user), - author: { - email: commit.author_email, - name: commit.author_name, - time: commit.authored_date - }, - committer: committer, - tree: cherry_pick_tree_id, - parents: [start_commit.sha]) + with_cache_hooks do + raw_repository.cherry_pick( + user: user, + commit: commit.raw, + branch_name: branch_name, + message: message, + start_branch_name: start_branch_name, + start_repository: start_project.repository.raw_repository + ) end end @@ -918,36 +887,6 @@ class Repository end end - def check_revert_content(target_commit, source_sha) - args = [target_commit.sha, source_sha] - args << { mainline: 1 } if target_commit.merge_commit? - - revert_index = rugged.revert_commit(*args) - return false if revert_index.conflicts? - - tree_id = revert_index.write_tree(rugged) - return false unless diff_exists?(source_sha, tree_id) - - tree_id - end - - def check_cherry_pick_content(target_commit, source_sha) - args = [target_commit.sha, source_sha] - args << 1 if target_commit.merge_commit? - - cherry_pick_index = rugged.cherrypick_commit(*args) - return false if cherry_pick_index.conflicts? - - tree_id = cherry_pick_index.write_tree(rugged) - return false unless diff_exists?(source_sha, tree_id) - - tree_id - end - - def diff_exists?(sha1, sha2) - rugged.diff(sha1, sha2).size > 0 - end - def merged_to_root_ref?(branch_name) branch_commit = commit(branch_name) root_ref_commit = commit(root_ref) @@ -983,6 +922,7 @@ class Repository def empty_repo? !exists? || !has_visible_content? end + cache_method :empty_repo?, memoize_only: true def search_files_by_content(query, ref) return [] if empty_repo? || query.blank? @@ -1112,12 +1052,6 @@ class Repository blob.data end - def refs_directory_exists? - circuit_breaker.perform do - File.exist?(File.join(path_to_repo, 'refs')) - end - end - def cache # TODO: should we use UUIDs here? We could move repositories without clearing this cache @cache ||= RepositoryCache.new(full_path, @project.id) @@ -1169,10 +1103,6 @@ class Repository Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false)) end - def circuit_breaker - @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage) - end - def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) ref ||= root_ref diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb index fae1b64961a..f025f40994e 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed_project.rb @@ -4,6 +4,7 @@ module Storage delegate :gitlab_shell, :repository_storage_path, to: :project ROOT_PATH_PREFIX = '@hashed'.freeze + STORAGE_VERSION = 1 def initialize(project) @project = project diff --git a/app/models/user.rb b/app/models/user.rb index 09c9b3250eb..4d523aa983f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -60,7 +60,7 @@ class User < ActiveRecord::Base lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i) return unless lease.try_obtain - Users::UpdateService.new(self).execute(validate: false) + Users::UpdateService.new(self, user: self).execute(validate: false) end attr_accessor :force_random_password @@ -130,6 +130,8 @@ class User < ActiveRecord::Base has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent + has_many :custom_attributes, class_name: 'UserCustomAttribute' + # # Validations # @@ -526,8 +528,8 @@ class User < ActiveRecord::Base def update_emails_with_primary_email primary_email_record = emails.find_by(email: email) if primary_email_record - Emails::DestroyService.new(self, email: email).execute - Emails::CreateService.new(self, email: email_was).execute + Emails::DestroyService.new(self, user: self, email: email).execute + Emails::CreateService.new(self, user: self, email: email_was).execute end end @@ -1000,7 +1002,7 @@ class User < ActiveRecord::Base if attempts_exceeded? lock_access! unless access_locked? else - Users::UpdateService.new(self).execute(validate: false) + Users::UpdateService.new(self, user: self).execute(validate: false) end end @@ -1186,7 +1188,7 @@ class User < ActiveRecord::Base &creation_block ) - Users::UpdateService.new(user).execute(validate: false) + Users::UpdateService.new(user, user: user).execute(validate: false) user ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb new file mode 100644 index 00000000000..eff25b31f9b --- /dev/null +++ b/app/models/user_custom_attribute.rb @@ -0,0 +1,6 @@ +class UserCustomAttribute < ActiveRecord::Base + belongs_to :user + + validates :user_id, :key, :value, presence: true + validates :key, uniqueness: { scope: [:user_id] } +end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 1be7bbe9953..64e550d19d0 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -11,6 +11,8 @@ class GlobalPolicy < BasePolicy with_options scope: :user, score: 0 condition(:access_locked) { @user.access_locked? } + condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } } + rule { anonymous }.policy do prevent :log_in prevent :access_api @@ -40,6 +42,10 @@ class GlobalPolicy < BasePolicy enable :create_group end + rule { can_create_fork }.policy do + enable :create_fork + end + rule { access_locked }.policy do prevent :log_in end @@ -47,4 +53,9 @@ class GlobalPolicy < BasePolicy rule { ~(anonymous & restricted_public_level) }.policy do enable :read_users_list end + + rule { admin }.policy do + enable :read_custom_attribute + enable :update_custom_attribute + end end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb index 85b67f0a237..92213f0155e 100644 --- a/app/policies/namespace_policy.rb +++ b/app/policies/namespace_policy.rb @@ -1,10 +1,14 @@ class NamespacePolicy < BasePolicy rule { anonymous }.prevent_all + condition(:personal_project, scope: :subject) { @subject.kind == 'user' } + condition(:can_create_personal_project, scope: :user) { @user.can_create_project? } condition(:owner) { @subject.owner == @user } rule { owner | admin }.policy do enable :create_projects enable :admin_namespace end + + rule { personal_project & ~can_create_personal_project }.prevent :create_projects end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index d20de9b16a4..31a712ccc1b 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -2,110 +2,55 @@ module Ci class CreatePipelineService < BaseService attr_reader :pipeline - def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil) + SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Validate::Abilities, + Gitlab::Ci::Pipeline::Chain::Validate::Repository, + Gitlab::Ci::Pipeline::Chain::Validate::Config, + Gitlab::Ci::Pipeline::Chain::Skip, + Gitlab::Ci::Pipeline::Chain::Create].freeze + + def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block) @pipeline = Ci::Pipeline.new( source: source, project: project, ref: ref, sha: sha, before_sha: before_sha, - tag: tag?, + tag: tag_exists?, trigger_requests: Array(trigger_request), user: current_user, pipeline_schedule: schedule, protected: project.protected_for?(ref) ) - result = validate_project_and_git_items || - validate_pipeline(ignore_skip_ci: ignore_skip_ci, - save_on_errors: save_on_errors) + command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci, + save_incompleted: save_on_errors, + seeds_block: block, + project: project, + current_user: current_user) - return result if result + sequence = Gitlab::Ci::Pipeline::Chain::Sequence + .new(pipeline, command, SEQUENCE) - begin - Ci::Pipeline.transaction do - pipeline.save! + sequence.build! do |pipeline, sequence| + update_merge_requests_head_pipeline if pipeline.persisted? - yield(pipeline) if block_given? + if sequence.complete? + cancel_pending_pipelines if project.auto_cancel_pending_pipelines? + pipeline_created_counter.increment(source: source) - Ci::CreatePipelineStagesService - .new(project, current_user) - .execute(pipeline) + pipeline.process! end - rescue ActiveRecord::RecordInvalid => e - return error("Failed to persist the pipeline: #{e}") end - - update_merge_requests_head_pipeline - - cancel_pending_pipelines if project.auto_cancel_pending_pipelines? - - pipeline_created_counter.increment(source: source) - - pipeline.tap(&:process!) end private - def validate_project_and_git_items - unless project.builds_enabled? - return error('Pipeline is disabled') - end - - unless allowed_to_trigger_pipeline? - if can?(current_user, :create_pipeline, project) - return error("Insufficient permissions for protected ref '#{ref}'") - else - return error('Insufficient permissions to create a new pipeline') - end - end - - unless branch? || tag? - return error('Reference not found') - end - - unless commit - return error('Commit not found') - end - end - - def validate_pipeline(ignore_skip_ci:, save_on_errors:) - unless pipeline.config_processor - unless pipeline.ci_yaml_file - return error("Missing #{pipeline.ci_yaml_file_path} file") - end - return error(pipeline.yaml_errors, save: save_on_errors) - end - - if !ignore_skip_ci && skip_ci? - pipeline.skip if save_on_errors - return pipeline - end - - unless pipeline.has_stage_seeds? - return error('No stages / jobs for this pipeline.') - end - end - - def allowed_to_trigger_pipeline? - if current_user - allowed_to_create? - else # legacy triggers don't have a corresponding user - !project.protected_for?(ref) - end + def commit + @commit ||= project.commit(origin_sha || origin_ref) end - def allowed_to_create? - return unless can?(current_user, :create_pipeline, project) - - access = Gitlab::UserAccess.new(current_user, project: project) - if branch? - access.can_update_branch?(ref) - elsif tag? - access.can_create_tag?(ref) - else - true # Allow it for now and we'll reject when we check ref existence - end + def sha + commit.try(:id) end def update_merge_requests_head_pipeline @@ -115,11 +60,6 @@ module Ci .update_all(head_pipeline_id: @pipeline.id) end - def skip_ci? - return false unless pipeline.git_commit_message - pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i - end - def cancel_pending_pipelines Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables| cancelables.find_each do |cancelable| @@ -136,14 +76,6 @@ module Ci .created_or_pending end - def commit - @commit ||= project.commit(origin_sha || origin_ref) - end - - def sha - commit.try(:id) - end - def before_sha params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA end @@ -156,41 +88,17 @@ module Ci params[:ref] end - def branch? - return @is_branch if defined?(@is_branch) - - @is_branch = - project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref) - end - - def tag? - return @is_tag if defined?(@is_tag) - - @is_tag = - project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref) + def tag_exists? + project.repository.tag_exists?(ref) end def ref @ref ||= Gitlab::Git.ref_name(origin_ref) end - def valid_sha? - origin_sha && origin_sha != Gitlab::Git::BLANK_SHA - end - - def error(message, save: false) - pipeline.tap do - pipeline.errors.add(:base, message) - - if save - pipeline.drop - update_merge_requests_head_pipeline - end - end - end - def pipeline_created_counter - @pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_total, "Counter of pipelines created") + @pipeline_created_counter ||= Gitlab::Metrics + .counter(:pipelines_created_total, "Counter of pipelines created") end end end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 85c2fcf9ea6..b9d0173a2d0 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -12,14 +12,18 @@ module Commits raise NotImplementedError unless repository.respond_to?(action) # rubocop:disable GitlabSecurity/PublicSend + message = @commit.public_send(:"#{action}_message", current_user) + + # rubocop:disable GitlabSecurity/PublicSend repository.public_send( action, current_user, @commit, @branch_name, + message, start_project: @start_project, start_branch_name: @start_branch) - rescue Repository::CreateTreeError + rescue Gitlab::Git::Repository::CreateTreeError error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content." raise ChangeError, error_msg diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index ff11bd59d29..077268b2388 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -6,15 +6,18 @@ class DeleteMergedBranchesService < BaseService def execute raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project) - branches = project.repository.branch_names - branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) } - # Prevent deletion of branches relevant to open merge requests - branches -= merge_request_branch_names - # Prevent deletion of protected branches - branches = branches.reject { |branch| project.protected_for?(branch) } + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37438 + Gitlab::GitalyClient.allow_n_plus_1_calls do + branches = project.repository.branch_names + branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) } + # Prevent deletion of branches relevant to open merge requests + branches -= merge_request_branch_names + # Prevent deletion of protected branches + branches = branches.reject { |branch| project.protected_for?(branch) } - branches.each do |branch| - DeleteBranchService.new(project, current_user).execute(branch) + branches.each do |branch| + DeleteBranchService.new(project, current_user).execute(branch) + end end end diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb index ace49889097..7f591c89411 100644 --- a/app/services/emails/base_service.rb +++ b/app/services/emails/base_service.rb @@ -1,7 +1,8 @@ module Emails class BaseService - def initialize(user, opts) - @user = user + def initialize(current_user, opts) + @current_user = current_user + @user = opts.delete(:user) @email = opts[:email] end end diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb index d586b9dfe0c..44011cc36c8 100644 --- a/app/services/emails/destroy_service.rb +++ b/app/services/emails/destroy_service.rb @@ -1,13 +1,13 @@ module Emails class DestroyService < ::Emails::BaseService def execute - Email.find_by_email!(@email).destroy && update_secondary_emails! + update_secondary_emails! if Email.find_by_email!(@email).destroy end private def update_secondary_emails! - result = ::Users::UpdateService.new(@user).execute do |user| + result = ::Users::UpdateService.new(@current_user, user: @user).execute do |user| user.update_secondary_emails! end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 8b967b78052..12604e7eb5d 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -182,6 +182,7 @@ class IssuableBaseService < BaseService after_create(issuable) execute_hooks(issuable) invalidate_cache_counts(issuable, users: issuable.assignees) + issuable.update_project_counter_caches end issuable @@ -193,8 +194,6 @@ class IssuableBaseService < BaseService def after_create(issuable) # To be overridden by subclasses - - issuable.update_project_counter_caches end def before_update(issuable) @@ -203,8 +202,6 @@ class IssuableBaseService < BaseService def after_update(issuable) # To be overridden by subclasses - - issuable.update_project_counter_caches end def update(issuable) @@ -229,6 +226,10 @@ class IssuableBaseService < BaseService before_update(issuable) + # We have to perform this check before saving the issuable as Rails resets + # the changed fields upon calling #save. + update_project_counters = issuable.update_project_counter_caches? + if issuable.with_transaction_returning_status { issuable.save } # We do not touch as it will affect a update on updated_at field ActiveRecord::Base.no_touching do @@ -249,6 +250,8 @@ class IssuableBaseService < BaseService after_update(issuable) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') + + issuable.update_project_counter_caches if update_project_counters end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index 74459c3342c..0c5cf2c62ad 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -29,6 +29,7 @@ module Issues todo_service.close_issue(issue, current_user) execute_hooks(issue, 'close') invalidate_cache_counts(issue, users: issue.assignees) + issue.update_project_counter_caches end issue diff --git a/app/services/keys/last_used_service.rb b/app/services/keys/last_used_service.rb new file mode 100644 index 00000000000..066f3246158 --- /dev/null +++ b/app/services/keys/last_used_service.rb @@ -0,0 +1,33 @@ +module Keys + class LastUsedService + TIMEOUT = 1.day.to_i + + attr_reader :key + + # key - The Key for which to update the last used timestamp. + def initialize(key) + @key = key + end + + def execute + # We _only_ want to update last_used_at and not also updated_at (which + # would be updated when using #touch). + key.update_column(:last_used_at, Time.zone.now) if update? + end + + def update? + last_used = key.last_used_at + + return false if last_used && (Time.zone.now - last_used) <= TIMEOUT + + !!redis_lease.try_obtain + end + + private + + def redis_lease + Gitlab::ExclusiveLease + .new("key_update_last_used_at:#{key.id}", timeout: TIMEOUT) + end + end +end diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index c0ce01f7523..40213c99014 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -14,6 +14,7 @@ module MergeRequests todo_service.close_merge_request(merge_request, current_user) execute_hooks(merge_request, 'close') invalidate_cache_counts(merge_request, users: merge_request.assignees) + merge_request.update_project_counter_caches end merge_request diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 3d53fe0646b..820709583fa 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -13,7 +13,10 @@ module MergeRequests merge_request.source_branch = params[:source_branch] merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) - create(merge_request) + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37439 + Gitlab::GitalyClient.allow_n_plus_1_calls do + create(merge_request) + end end def before_create(merge_request) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 07cbd8f92a9..bf26859dd6d 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -14,13 +14,13 @@ module MergeRequests @merge_request = merge_request unless @merge_request.mergeable? - return log_merge_error('Merge request is not mergeable', save_message_on_model: true) + return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true) end @source = find_merge_source unless @source - return log_merge_error('No source for merge', save_message_on_model: true) + return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true) end merge_request.in_locked_state do @@ -31,8 +31,7 @@ module MergeRequests end end rescue MergeError => e - clean_merge_jid - log_merge_error(e.message, save_message_on_model: true) + handle_merge_error(log_message: e.message, save_message_on_model: true) end private @@ -74,10 +73,16 @@ module MergeRequests @merge_request.force_remove_source_branch? ? @merge_request.author : current_user end - def log_merge_error(message, save_message_on_model: false) - Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}") + # Logs merge error message and cleans `MergeRequest#merge_jid`. + # + def handle_merge_error(log_message:, save_message_on_model: false) + Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}") - @merge_request.update(merge_error: message) if save_message_on_model + if save_message_on_model + @merge_request.update(merge_error: log_message, merge_jid: nil) + else + clean_merge_jid + end end def merge_request_info diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 261a8bfa200..b1d6bac4d4a 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -14,6 +14,7 @@ module MergeRequests notification_service.merge_mr(merge_request, current_user) execute_hooks(merge_request, 'merge') invalidate_cache_counts(merge_request, users: merge_request.assignees) + merge_request.update_project_counter_caches end private diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 06971483992..9ea28733f5f 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -4,7 +4,13 @@ module Notes merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha) note = Notes::BuildService.new(project, current_user, params).execute - return note unless note.valid? + + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37440 + note_valid = Gitlab::GitalyClient.allow_n_plus_1_calls do + note.valid? + end + + return note unless note_valid # We execute commands (extracted from `params[:note]`) on the noteable # **before** we save the note because if the note consists of commands diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb index 5e633c37bf8..aa034315280 100644 --- a/app/services/projects/count_service.rb +++ b/app/services/projects/count_service.rb @@ -2,6 +2,11 @@ module Projects # Base class for the various service classes that count project data (e.g. # issues or forks). class CountService + # The version of the cache format. This should be bumped whenever the + # underlying logic changes. This removes the need for explicitly flushing + # all caches. + VERSION = 1 + def initialize(project) @project = project end @@ -37,7 +42,7 @@ module Projects end def cache_key - ['projects', @project.id, cache_key_name] + ['projects', 'count_service', VERSION, @project.id, cache_key_name] end end end diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb new file mode 100644 index 00000000000..41259de3a16 --- /dev/null +++ b/app/services/projects/hashed_storage_migration_service.rb @@ -0,0 +1,68 @@ +module Projects + class HashedStorageMigrationService < BaseService + include Gitlab::ShellAdapter + + attr_reader :old_disk_path, :new_disk_path + + def initialize(project, logger = nil) + @project = project + @logger ||= Rails.logger + end + + def execute + return if project.hashed_storage? + + @old_disk_path = project.disk_path + has_wiki = project.wiki.repository_exists? + + project.storage_version = Storage::HashedProject::STORAGE_VERSION + project.ensure_storage_path_exists + + @new_disk_path = project.disk_path + + result = move_repository(@old_disk_path, @new_disk_path) + + if has_wiki + result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki") + end + + unless result + rollback_folder_move + return + end + + project.repository_read_only = false + project.save! + + block_given? ? yield : result + end + + private + + def move_repository(from_name, to_name) + from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git") + to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git") + + # If we don't find the repository on either original or target we should log that as it could be an issue if the + # project was not originally empty. + if !from_exists && !to_exists + logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..." + return false + elsif !from_exists + # Repository have been moved already. + return true + end + + gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) + end + + def rollback_folder_move + move_repository(@new_disk_path, @old_disk_path) + move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki") + end + + def logger + @logger + end + end +end diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index b3f4a72d6fe..cc76d0df3a1 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -11,7 +11,7 @@ module Tags begin new_tag = repository.add_tag(current_user, tag_name, target, message) - rescue Rugged::TagError + rescue Gitlab::Git::Repository::TagExistsError return error("Tag #{tag_name} already exists") rescue Gitlab::Git::HooksService::PreReceiveError => ex return error(ex.message) diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb index 6188b8a4349..15ca1a55a5b 100644 --- a/app/services/users/update_service.rb +++ b/app/services/users/update_service.rb @@ -2,22 +2,21 @@ module Users class UpdateService < BaseService include NewUserNotifier - def initialize(user, params = {}) - @user = user + def initialize(current_user, params = {}) + @current_user = current_user + @user = params.delete(:user) @params = params.dup end def execute(validate: true, &block) yield(@user) if block_given? - assign_attributes(&block) - user_exists = @user.persisted? - if @user.save(validate: validate) - notify_new_user(@user, nil) unless user_exists + assign_attributes(&block) - success + if @user.save(validate: validate) + notify_success(user_exists) else error(@user.errors.full_messages.uniq.join('. ')) end @@ -33,6 +32,12 @@ module Users private + def notify_success(user_exists) + notify_new_user(@user, nil) unless user_exists + + success + end + def assign_attributes(&block) if @user.user_synced_attributes_metadata params.except!(*@user.user_synced_attributes_metadata.read_only_attributes) diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 66d3bcb998a..cbb79376d5f 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -9,7 +9,7 @@ class AvatarUploader < GitlabUploader end def exists? - model.avatar.file && model.avatar.file.exists? + model.avatar.file && model.avatar.file.present? end # We set move_to_store and move_to_cache to 'false' to prevent stealing diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 05a2091633a..7f72b3ce471 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -51,7 +51,7 @@ class GitlabUploader < CarrierWave::Uploader::Base end def exists? - file.try(:exists?) + file.present? end # Override this if you don't want to save files by default to the Rails.root directory diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index e5842bd1ea0..3ef8f2a3acb 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Background Jobs" -= render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title Background Jobs diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml index bff53da1d9a..5e9a8c083af 100644 --- a/app/views/admin/cohorts/index.html.haml +++ b/app/views/admin/cohorts/index.html.haml @@ -1,6 +1,5 @@ - breadcrumb_title "Cohorts" - @no_container = true -= render "admin/dashboard/head" %div{ class: container_class } - if @cohorts diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml index 833d4c612f8..30dd87f0463 100644 --- a/app/views/admin/conversational_development_index/show.html.haml +++ b/app/views/admin/conversational_development_index/show.html.haml @@ -1,8 +1,6 @@ - @no_container = true - page_title 'ConvDev Index' -= render 'admin/monitoring/head' - .container - if show_callout?('convdev_intro_callout_dismissed') = render 'callout' diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml deleted file mode 100644 index c2151710884..00000000000 --- a/app/views/admin/dashboard/_head.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = 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: :conversational_development_index) do - = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do - %span - ConvDev Index diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 703f4165128..2f0143c7eff 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,13 +1,12 @@ - @no_container = true - breadcrumb_title "Dashboard" -= render "admin/dashboard/head" %div{ class: container_class } .admin-dashboard.prepend-top-default .row .col-md-4 .info-well - .well-segment.admin-well + .well-segment.admin-well.admin-well-statistics %h4 Statistics %p Forks @@ -43,7 +42,7 @@ = number_with_delimiter(User.active.count) .col-md-4 .info-well - .well-segment.admin-well + .well-segment.admin-well.admin-well-features %h4 Features - sign_up = "Sign up" %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") } @@ -111,6 +110,10 @@ GitLab API %span.pull-right = API::API::version + %p + Gitaly + %span.pull-right + = Gitlab::GitalyClient.expected_server_version - if Gitlab.config.pages.enabled %p GitLab Pages diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index e5f380c78e2..535251fef5e 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Groups" -= render "admin/dashboard/head" %div{ class: container_class } .top-area diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 517db50b97f..10a3bed0a4f 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title _('Health Check') - no_errors = @errors.blank? && @failing_storage_statuses.blank? -= render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title= page_title diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml index aa6e9db3900..0310498ae54 100644 --- a/app/views/admin/jobs/index.html.haml +++ b/app/views/admin/jobs/index.html.haml @@ -1,6 +1,5 @@ - breadcrumb_title "Jobs" - @no_container = true -= render "admin/dashboard/head" %div{ class: container_class } diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index ee87f25a225..78757b6384f 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Logs" -= render 'admin/monitoring/head' %div{ class: container_class } %ul.nav-links.log-tabs diff --git a/app/views/admin/monitoring/_head.html.haml b/app/views/admin/monitoring/_head.html.haml deleted file mode 100644 index b3530915068..00000000000 --- a/app/views/admin/monitoring/_head.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = 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 diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 3301f55b8a8..3f202fbf4fe 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -2,7 +2,6 @@ - page_title "Projects" - params[:visibility_level] ||= [] -= render "admin/dashboard/head" %div{ class: container_class } .top-area diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml index b7db18b2d32..cb02a750490 100644 --- a/app/views/admin/requests_profiles/index.html.haml +++ b/app/views/admin/requests_profiles/index.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title 'Requests Profiles' -= render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 6793ce557c4..43cea1358cc 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,6 +1,5 @@ - breadcrumb_title "Runners" - @no_container = true -= render "admin/dashboard/head" %div{ class: container_class } .bs-callout diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index fd0281e4961..6bf979a937e 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "System Info" -= render 'admin/monitoring/head' %div{ class: container_class } .prepend-top-default diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 5516134d8a0..38ce1564eff 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Users" -= render "admin/dashboard/head" %div{ class: container_class } .prepend-top-default diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml new file mode 100644 index 00000000000..3701e1c0578 --- /dev/null +++ b/app/views/dashboard/projects/_nav.html.haml @@ -0,0 +1,6 @@ +.top-area + %ul.nav-links + = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do + = link_to s_('DashboardProjects|All'), dashboard_projects_path + = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do + = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true) diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index c546252455a..57a4da353fe 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -10,11 +10,9 @@ = render "projects/last_push" %div{ class: container_class } - - if show_callout?('user_callout_dismissed') - = render 'shared/user_callout' - - - if has_projects_or_name?(@projects, params) + - if show_projects?(@projects, params) = render 'dashboard/projects_head' + = render 'nav' = render 'projects' - else = render "zero_authorized_projects" diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 9b615ec999e..f62a0cd681e 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -77,13 +77,14 @@ %ul.content-list.todos-list = render @todos = paginate @todos, theme: "gitlab" - .js-nothing-here-container.todos-all-done.hidden - = render "shared/empty_states/icons/todos_all_done.svg" + .js-nothing-here-container.todos-all-done.hidden.svg-content + = image_tag 'illustrations/todos_all_done.svg' %h4.text-center You're all done! - elsif current_user.todos.any? .todos-all-done - = render "shared/empty_states/icons/todos_all_done.svg" + .svg-content + = image_tag 'illustrations/todos_all_done.svg' - if todos_filter_empty? %h4.text-center = Gitlab.config.gitlab.no_todos_messages.sample @@ -99,8 +100,8 @@ There are no todos to show. - else .todos-empty - .todos-empty-hero - = render "shared/empty_states/icons/todos_empty.svg" + .todos-empty-hero.svg-content + = image_tag 'illustrations/todos_empty.svg' .todos-empty-content %h4 Todos let you see what you should do next. diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml index a508b7537a2..4d1037807be 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.haml +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -1,9 +1,10 @@ +- confirmation_link = confirmation_url(@resource, confirmation_token: @token) - if @resource.unconfirmed_email.present? #content = email_default_heading(@resource.unconfirmed_email) %p Click the link below to confirm your email address. #cta - = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) + = link_to confirmation_link, confirmation_link - else #content - if Gitlab.com? @@ -12,4 +13,4 @@ = email_default_heading("Welcome, #{@resource.name}!") %p To get started, click the link below to confirm your account. #cta - = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) + = link_to confirmation_link, confirmation_link diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml index cab346fb514..50dd5864195 100644 --- a/app/views/discussions/_new_issue_for_all_discussions.html.haml +++ b/app/views/discussions/_new_issue_for_all_discussions.html.haml @@ -1,6 +1,8 @@ - if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project) .btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" } - .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve all discussions in new issue", - "aria-label" => "Resolve all discussions in a new issue", - "data-container" => "body" } - = link_to custom_icon('icon_mr_issue'), new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid), title: "Resolve all discussions in new issue", class: 'new-issue-for-discussion' + = link_to custom_icon('icon_mr_issue'), + new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid), + title: 'Resolve all discussions in new issue', + aria: { label: 'Resolve all discussions in new issue' }, + data: { container: 'body' }, + class: 'new-issue-for-discussion btn btn-default discussion-create-issue-btn has-tooltip' diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml index a9bc317b8f8..2bfe118c608 100644 --- a/app/views/discussions/_new_issue_for_discussion.html.haml +++ b/app/views/discussions/_new_issue_for_discussion.html.haml @@ -2,7 +2,9 @@ %new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'", "inline-template" => true } .btn-group{ role: "group", "v-if" => "showButton" } - .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve this discussion in a new issue", - "aria-label" => "Resolve this discussion in a new issue", - "data-container" => "body" } - = link_to custom_icon('icon_mr_issue'), new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), title: "Resolve this discussion in a new issue", class: 'new-issue-for-discussion' + = link_to custom_icon('icon_mr_issue'), + new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), + title: 'Resolve this discussion in a new issue', + aria: { label: 'Resolve this discussion in a new issue' }, + data: { container: 'body' }, + class: 'new-issue-for-discussion btn btn-default discussion-create-issue-btn has-tooltip' diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg deleted file mode 100644 index 1522c9d51c9..00000000000 --- a/app/views/feature_highlight/_issue_boards.svg +++ /dev/null @@ -1,98 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> - <filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox"> - <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> - <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> - </filter> - <path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> - <filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> - <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> - <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> - </filter> - <path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> - <path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> - <filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> - <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> - <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> - </filter> - <path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> - <filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> - <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> - <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> - </filter> - <path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> - <filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> - <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> - <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> - </filter> - <path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> - <filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> - <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> - <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> - </filter> - <path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/> - <filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox"> - <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> - <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/> - </filter> - </defs> - <g fill="none" fill-rule="evenodd"> - <path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/> - <g transform="translate(11 23)"> - <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> - <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/> - <g transform="translate(5 10)"> - <use fill="black" filter="url(#a)" xlink:href="#b"/> - <use fill="#F9F9F9" xlink:href="#b"/> - </g> - <g transform="translate(5 42)"> - <use fill="black" filter="url(#c)" xlink:href="#d"/> - <use fill="#FEF0E8" xlink:href="#d"/> - <path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/> - <path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/> - <path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/> - </g> - </g> - <path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/> - <g transform="translate(145 28)"> - <mask id="f" fill="white"> - <use xlink:href="#e"/> - </mask> - <use fill="#FFFFFF" xlink:href="#e"/> - <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/> - <g transform="translate(5 10)"> - <use fill="black" filter="url(#g)" xlink:href="#h"/> - <use fill="#F9F9F9" xlink:href="#h"/> - </g> - <g transform="translate(5 42)"> - <use fill="black" filter="url(#i)" xlink:href="#j"/> - <use fill="#FEF0E8" xlink:href="#j"/> - <path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/> - <path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/> - <path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/> - </g> - </g> - <path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/> - <g transform="translate(78 16)"> - <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/> - <g transform="translate(5 10)"> - <use fill="black" filter="url(#k)" xlink:href="#l"/> - <use fill="#EFEDF8" xlink:href="#l"/> - <path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/> - <path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/> - <path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/> - </g> - <g transform="translate(5 42)"> - <use fill="black" filter="url(#m)" xlink:href="#n"/> - <use fill="#F9F9F9" xlink:href="#n"/> - </g> - <g transform="translate(5 74)"> - <rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/> - <use fill="black" filter="url(#o)" xlink:href="#p"/> - <use fill="#F9F9F9" xlink:href="#p"/> - </g> - <path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/> - </g> - </g> -</svg> diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml deleted file mode 100644 index 0f63774fb9b..00000000000 --- a/app/views/groups/_head.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: container_class } - = 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 - -.hidden-xs - = render "projects/last_push" diff --git a/app/views/groups/_head_issues.html.haml b/app/views/groups/_head_issues.html.haml deleted file mode 100644 index d554bc23743..00000000000 --- a/app/views/groups/_head_issues.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: container_class } - = 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 diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml deleted file mode 100644 index 623d233a46a..00000000000 --- a/app/views/groups/_settings_head.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: container_class } - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'General' do - %span - General - - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do - %span - Projects - - = nav_link(controller: :ci_cd) do - = link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do - %span - Pipelines diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml index 3969e56f937..cb7dab26332 100644 --- a/app/views/groups/activity.html.haml +++ b/app/views/groups/activity.html.haml @@ -2,7 +2,6 @@ = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") - page_title "Activity" -= render 'groups/head' %section.activities = render 'activities' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 0d3308833b7..15606dd30fd 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,5 +1,4 @@ - breadcrumb_title "General Settings" -= render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading Group settings diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 7f411927429..07e64d9aeaf 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,6 +1,5 @@ - page_title "Issues" - group_issues_exists = group_issues(@group).exists? -= render "head_issues" = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 89165096fe2..d10efdad53b 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,8 +1,5 @@ - page_title 'Labels' -= render "groups/head_issues" - - .top-area.adjust .nav-text Labels can be applied to issues and merge requests. Group labels are available for any project within the group. diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index ed582e521c4..cb4fc69d5b8 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,7 +1,5 @@ - page_title "Milestones" -= render "groups/head_issues" - .top-area = render 'shared/milestones_filter', counts: @milestone_states diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 7f3f2f707f7..8d2bc810a7d 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,5 +1,4 @@ - breadcrumb_title "Projects" -= render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 9f9ae01e7c5..472da2a6a72 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -1,5 +1,4 @@ - breadcrumb_title "CI / CD Settings" - page_title "CI / CD" -= render "groups/settings_head" = render 'ci/variables/index' diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index f4f76887422..3ca63f9c3e0 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -4,7 +4,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") -= render 'groups/head' = render 'groups/home_panel' .groups-header{ class: container_class } diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml index 7abc84412c6..869b3b243c6 100644 --- a/app/views/groups/subgroups.html.haml +++ b/app/views/groups/subgroups.html.haml @@ -1,7 +1,6 @@ - breadcrumb_title "Details" - @no_container = true -= render 'head' = render 'groups/home_panel' .groups-header{ class: container_class } diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index c07c148a12a..d6789baea28 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,5 @@ +- content_for :page_specific_javascripts do + = webpack_bundle_tag 'help' - page_title @path.split("/").reverse.map(&:humanize) .documentation.wiki.prepend-top-default = markdown @markdown diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index e6a10e500a4..e3a9e99250e 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -32,10 +32,6 @@ = stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled? - // TODO: Combine these 2 stylesheets into application.scss - = stylesheet_link_tag "new_nav", media: "all" - = stylesheet_link_tag "new_sidebar", media: "all" - = Gon::Base.render_data - if content_for?(:library_javascripts) diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index cd7a47da4a1..29387d6627e 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -21,8 +21,8 @@ %a Loading... = dropdown_loading - %i.search-icon - %i.clear-icon.js-clear-input + = sprite_icon('search', size: 16, css_class: 'search-icon') + = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input') = hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index d8fc371497d..7e9b76da570 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -22,29 +22,29 @@ = 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') + = sprite_icon('search', size: 16) - if current_user - %li.user-counter + = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = custom_icon('issues') + = sprite_icon('issues', size: 16) - issues_count = assigned_issuables_count(:issues) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) - %li.user-counter + = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = custom_icon('mr_bold') + = sprite_icon('git-merge', size: 16) - 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.user-counter + = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = custom_icon('todo_done') + = sprite_icon('todo-done', size: 16) %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: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar" - = custom_icon('caret_down') + = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul %li.current-user @@ -73,7 +73,7 @@ %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') + = sprite_icon('more', size: 16, css_class: 'more-icon js-navbar-toggle-right') + = sprite_icon('close', size: 16, css_class: 'close-icon js-navbar-toggle-left') = render 'shared/outdated_browser' diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 63d1c077ecd..088f2785092 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,7 +1,7 @@ %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 - = custom_icon('plus_square') - = custom_icon('caret_down') + = sprite_icon('plus-square', size: 16) + = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul - if @group&.persisted? diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index 7bd3f5306a2..002922e13f1 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -16,5 +16,5 @@ = breadcrumb_list_item link_to(extra[:text], extra[:link]) = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after %li - %h2.breadcrumbs-sub-title= @breadcrumb_title + %h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link = yield :header_content diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index c254ee02dd8..e0d8d9cb402 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -2,7 +2,7 @@ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do %a{ href: "#", data: { toggle: "dropdown" } } Projects - = custom_icon('caret_down') + = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.projects-dropdown-menu = render "layouts/nav/projects_dropdown/show" @@ -25,7 +25,7 @@ %li.header-more.dropdown.hidden-lg %a{ href: "#", data: { toggle: "dropdown" } } More - = custom_icon('caret_down') + = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu %ul = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do @@ -54,7 +54,7 @@ - if current_user.admin? = nav_link(controller: 'admin/dashboard') do = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('wrench fw') + = sprite_icon('admin', size: 18) - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions', diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml index 28022eebb19..610ff9001f7 100644 --- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml +++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml @@ -4,7 +4,7 @@ %li.dropdown %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip } = icon("ellipsis-h") - = icon("angle-right", class: "breadcrumbs-list-angle") + = sprite_icon("angle-right", css_class: "breadcrumbs-list-angle") .dropdown-menu %ul - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index| diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 615238b94ad..0ec07605631 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -3,16 +3,15 @@ .context-header = link_to admin_root_path, title: 'Admin Overview' do .avatar-container.s40.settings-avatar - = icon('wrench') + = sprite_icon('admin', size: 24) .sidebar-context-title Admin Area %ul.sidebar-top-level-items = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do = link_to admin_root_path, class: 'shortcuts-tree' do .nav-icon-container - = custom_icon('overview') + = sprite_icon('overview') %span.nav-item-name Overview - %ul.sidebar-sub-level-items = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do = link_to admin_root_path do @@ -55,7 +54,7 @@ = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do = link_to admin_system_info_path do .nav-icon-container - = custom_icon('monitoring') + = sprite_icon('monitor') %span.nav-item-name Monitoring @@ -89,7 +88,7 @@ = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path do .nav-icon-container - = custom_icon('messages') + = sprite_icon('messages') %span.nav-item-name Messages %ul.sidebar-sub-level-items.is-fly-out-only @@ -101,7 +100,7 @@ = nav_link(controller: [:hooks, :hook_logs]) do = link_to admin_hooks_path do .nav-icon-container - = custom_icon('system_hooks') + = sprite_icon('hook') %span.nav-item-name System Hooks %ul.sidebar-sub-level-items.is-fly-out-only @@ -113,7 +112,7 @@ = nav_link(controller: :applications) do = link_to admin_applications_path do .nav-icon-container - = custom_icon('applications') + = sprite_icon('applications') %span.nav-item-name Applications %ul.sidebar-sub-level-items.is-fly-out-only @@ -125,7 +124,7 @@ = nav_link(controller: :abuse_reports) do = link_to admin_abuse_reports_path do .nav-icon-container - = custom_icon('abuse_reports') + = sprite_icon('slight-frown') %span.nav-item-name Abuse Reports %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) @@ -140,7 +139,7 @@ = nav_link(controller: :spam_logs) do = link_to admin_spam_logs_path do .nav-icon-container - = custom_icon('spam_logs') + = sprite_icon('spam') %span.nav-item-name Spam Logs %ul.sidebar-sub-level-items.is-fly-out-only @@ -152,7 +151,7 @@ = nav_link(controller: :deploy_keys) do = link_to admin_deploy_keys_path do .nav-icon-container - = custom_icon('key') + = sprite_icon('key') %span.nav-item-name Deploy Keys %ul.sidebar-sub-level-items.is-fly-out-only @@ -164,7 +163,7 @@ = nav_link(controller: :services) do = link_to admin_application_settings_services_path do .nav-icon-container - = custom_icon('service_templates') + = sprite_icon('template') %span.nav-item-name Service Templates %ul.sidebar-sub-level-items.is-fly-out-only @@ -176,7 +175,7 @@ = nav_link(controller: :labels) do = link_to admin_labels_path do .nav-icon-container - = custom_icon('labels') + = sprite_icon('labels') %span.nav-item-name Labels %ul.sidebar-sub-level-items.is-fly-out-only @@ -188,7 +187,7 @@ = nav_link(controller: :appearances) do = link_to admin_appearances_path do .nav-icon-container - = custom_icon('appearance') + = sprite_icon('appearance') %span.nav-item-name Appearance %ul.sidebar-sub-level-items.is-fly-out-only @@ -200,7 +199,7 @@ = nav_link(controller: :application_settings) do = link_to admin_application_settings_path do .nav-icon-container - = custom_icon('settings') + = sprite_icon('settings') %span.nav-item-name Settings %ul.sidebar-sub-level-items.is-fly-out-only diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index cb44c012f56..8cba495f7e4 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -13,7 +13,7 @@ = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do = link_to group_path(@group) do .nav-icon-container - = custom_icon('project') + = sprite_icon('project') %span.nav-item-name Overview @@ -36,7 +36,7 @@ = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do = link_to issues_group_path(@group) do .nav-icon-container - = custom_icon('issues') + = sprite_icon('issues') %span.nav-item-name Issues %span.badge.count= number_with_delimiter(issues.count) @@ -66,7 +66,7 @@ = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group) do .nav-icon-container - = custom_icon('mr_bold') + = sprite_icon('git-merge') %span.nav-item-name Merge Requests %span.badge.count= number_with_delimiter(merge_requests.count) @@ -79,7 +79,7 @@ = nav_link(path: 'group_members#index') do = link_to group_group_members_path(@group) do .nav-icon-container - = custom_icon('members') + = sprite_icon('users') %span.nav-item-name Members %ul.sidebar-sub-level-items.is-fly-out-only @@ -91,7 +91,7 @@ = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do = link_to edit_group_path(@group) do .nav-icon-container - = custom_icon('settings') + = sprite_icon('settings') %span.nav-item-name Settings %ul.sidebar-sub-level-items diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 2c402591f62..458b5010d36 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -3,13 +3,13 @@ .context-header = link_to profile_path, title: 'Profile Settings' do .avatar-container.s40.settings-avatar - = icon('user') + = sprite_icon('user', size: 24) .sidebar-context-title User Settings %ul.sidebar-top-level-items = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path do .nav-icon-container - = custom_icon('profile') + = sprite_icon('profile') %span.nav-item-name Profile %ul.sidebar-sub-level-items.is-fly-out-only @@ -20,7 +20,7 @@ = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path do .nav-icon-container - = custom_icon('account') + = sprite_icon('account') %span.nav-item-name Account %ul.sidebar-sub-level-items.is-fly-out-only @@ -32,7 +32,7 @@ = nav_link(controller: 'oauth/applications') do = link_to applications_profile_path do .nav-icon-container - = custom_icon('applications') + = sprite_icon('applications') %span.nav-item-name Applications %ul.sidebar-sub-level-items.is-fly-out-only @@ -43,7 +43,7 @@ = nav_link(controller: :chat_names) do = link_to profile_chat_names_path do .nav-icon-container - = custom_icon('chat') + = sprite_icon('comment') %span.nav-item-name Chat %ul.sidebar-sub-level-items.is-fly-out-only @@ -54,7 +54,7 @@ = nav_link(controller: :personal_access_tokens) do = link_to profile_personal_access_tokens_path do .nav-icon-container - = custom_icon('access_tokens') + = sprite_icon('token') %span.nav-item-name Access Tokens %ul.sidebar-sub-level-items.is-fly-out-only @@ -65,7 +65,7 @@ = nav_link(controller: :emails) do = link_to profile_emails_path do .nav-icon-container - = custom_icon('emails') + = sprite_icon('mail') %span.nav-item-name Emails %ul.sidebar-sub-level-items.is-fly-out-only @@ -77,7 +77,7 @@ = nav_link(controller: :passwords) do = link_to edit_profile_password_path do .nav-icon-container - = custom_icon('lock') + = sprite_icon('lock') %span.nav-item-name Password %ul.sidebar-sub-level-items.is-fly-out-only @@ -88,7 +88,7 @@ = nav_link(controller: :notifications) do = link_to profile_notifications_path do .nav-icon-container - = custom_icon('notifications') + = sprite_icon('notifications') %span.nav-item-name Notifications %ul.sidebar-sub-level-items.is-fly-out-only @@ -99,7 +99,7 @@ = nav_link(controller: :keys) do = link_to profile_keys_path do .nav-icon-container - = custom_icon('key') + = sprite_icon('key') %span.nav-item-name SSH Keys %ul.sidebar-sub-level-items.is-fly-out-only @@ -110,7 +110,7 @@ = nav_link(controller: :gpg_keys) do = link_to profile_gpg_keys_path do .nav-icon-container - = custom_icon('key_2') + = sprite_icon('key-2') %span.nav-item-name GPG Keys %ul.sidebar-sub-level-items.is-fly-out-only @@ -121,7 +121,7 @@ = nav_link(controller: :preferences) do = link_to profile_preferences_path do .nav-icon-container - = custom_icon('preferences') + = sprite_icon('preferences') %span.nav-item-name Preferences %ul.sidebar-sub-level-items.is-fly-out-only @@ -132,7 +132,7 @@ = nav_link(path: 'profiles#audit_log') do = link_to audit_log_profile_path do .nav-icon-container - = custom_icon('authentication_log') + = sprite_icon('log') %span.nav-item-name Authentication log %ul.sidebar-sub-level-items.is-fly-out-only diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 29f1fc6b354..8765b814405 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -11,7 +11,7 @@ = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do = link_to project_path(@project), class: 'shortcuts-project' do .nav-icon-container - = custom_icon('project') + = sprite_icon('project') %span.nav-item-name Overview @@ -38,7 +38,7 @@ = 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_tree_path(@project), class: 'shortcuts-tree' do .nav-icon-container - = custom_icon('doc_text') + = sprite_icon('doc_text') %span.nav-item-name Repository @@ -84,7 +84,7 @@ = nav_link(controller: %w[projects/registry/repositories]) do = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do .nav-icon-container - = custom_icon('container_registry') + = sprite_icon('disk') %span.nav-item-name Registry @@ -92,7 +92,7 @@ = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do = link_to project_issues_path(@project), class: 'shortcuts-issues' do .nav-icon-container - = custom_icon('issues') + = sprite_icon('issues') %span.nav-item-name Issues - if @project.issues_enabled? @@ -117,20 +117,6 @@ = link_to project_boards_path(@project), title: boards_link_text do %span = boards_link_text - .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } } - .feature-highlight-popover-content - = render 'feature_highlight/issue_boards.svg' - .feature-highlight-popover-sub-content - %span= _('Use') - = link_to 'Issue Boards', project_boards_path(@project) - %span= _('to create customized software development workflows like') - %strong= _('Scrum') - %span= _('or') - %strong= _('Kanban') - %hr - %button.btn-link.dismiss-feature-highlight{ type: 'button' } - %span= _("Got it! Don't show this again") - = custom_icon('thumbs_up') = nav_link(controller: :labels) do = link_to project_labels_path(@project), title: 'Labels' do @@ -146,7 +132,7 @@ = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do .nav-icon-container - = custom_icon('mr_bold') + = sprite_icon('git-merge') %span.nav-item-name Merge Requests %span.badge.count.merge_counter.js-merge-counter @@ -163,7 +149,7 @@ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do .nav-icon-container - = custom_icon('pipeline') + = sprite_icon('pipeline') %span.nav-item-name CI / CD @@ -207,7 +193,7 @@ = nav_link(controller: :wikis) do = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do .nav-icon-container - = custom_icon('wiki') + = sprite_icon('book') %span.nav-item-name Wiki %ul.sidebar-sub-level-items.is-fly-out-only @@ -220,7 +206,7 @@ = nav_link(controller: :snippets) do = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do .nav-icon-container - = custom_icon('snippets') + = sprite_icon('snippet') %span.nav-item-name Snippets %ul.sidebar-sub-level-items.is-fly-out-only @@ -233,7 +219,7 @@ = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do = link_to edit_project_path(@project), class: 'shortcuts-tree' do .nav-icon-container - = custom_icon('settings') + = sprite_icon('settings') %span.nav-item-name Settings @@ -277,7 +263,7 @@ = nav_link(path: %w[members#show]) do = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do .nav-icon-container - = custom_icon('members') + = sprite_icon('users') %span.nav-item-name Members diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 5d778d67ae7..8abbd828032 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -74,7 +74,9 @@ %h4.prepend-top-0.warning-title Change username %p - Changing your username will change path to all personal projects! + Changing your username can have unintended side effects. + = succeed '.' do + = link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank' .col-lg-8 = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f| .form-group diff --git a/app/views/projects/_head.html.haml b/app/views/projects/_head.html.haml deleted file mode 100644 index dba84838a52..00000000000 --- a/app/views/projects/_head.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: container_class } - = 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') diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index f80dadb8037..d0ab39033cf 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -2,8 +2,6 @@ - page_title _("Activity") -= render "projects/head" - = render 'projects/last_push' = render 'projects/activity' diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 4cc3218d967..fe02cbcbf95 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,10 +1,9 @@ - breadcrumb_title _('Artifacts') - page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' -= render "projects/pipelines/head" = render "projects/jobs/header", show_controls: false -- add_to_breadcrumbs(_('Jobs'), project_jobs_path(@project)) +- add_to_breadcrumbs(s_('CICD|Jobs'), project_jobs_path(@project)) - add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project)) .tree-holder diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml index b85bbcb980e..2942d618a42 100644 --- a/app/views/projects/artifacts/file.html.haml +++ b/app/views/projects/artifacts/file.html.haml @@ -1,5 +1,4 @@ - page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' -= render "projects/pipelines/head" = render "projects/jobs/header", show_controls: false diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 60ac202bde0..e45861ac08d 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,7 +1,6 @@ - @no_container = true - project_duration = age_map_duration(@blame_groups, @project) - page_title "Blame", @blob.path, @ref -= render "projects/commits/head" %div{ class: container_class } #blob-content-holder.tree-holder diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 992fe7f717f..626cbc9e41d 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -4,7 +4,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_bundle_tag('blob') -= render "projects/commits/head" %div{ class: container_class } - if @conflict diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 240e62d5ac5..c4712bf3736 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -2,7 +2,6 @@ - @no_container = true - page_title @blob.path, @ref -= render "projects/commits/head" - content_for :page_specific_javascripts do = webpack_bundle_tag 'blob' diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 05c1d2b383c..49101d1efa4 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -13,20 +13,23 @@ - if branch.name == @repository.root_ref %span.label.label-primary default - elsif @repository.merged_to_root_ref? branch.name - %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" } - merged + %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } + = s_('Branches|merged') - if protected_branch?(@project, branch) %span.label.label-success - protected + = s_('Branches|protected') .controls.hidden-xs< - if merge_project && create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do - Merge request + = _('Merge request') - if branch.name != @repository.root_ref - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do - Compare + = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), + class: "btn btn-default #{'prepend-left-10' unless merge_project}", + method: :post, + title: s_('Branches|Compare') do + = s_('Branches|Compare') = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name] @@ -34,12 +37,12 @@ - if branch.name == @project.repository.root_ref %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", disabled: true, - title: "The default branch cannot be deleted" } + title: s_('Branches|The default branch cannot be deleted') } = icon("trash-o") - elsif protected_branch?(@project, branch) - if can?(current_user, :delete_protected_branch, @project) %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", - title: "Delete protected branch", + title: s_('Branches|Delete protected branch'), data: { toggle: "modal", target: "#modal-delete-branch", delete_path: project_branch_path(@project, branch.name), @@ -49,20 +52,22 @@ - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", disabled: true, - title: "Only a project master or owner can delete a protected branch" } + title: s_('Branches|Only a project master or owner can delete a protected branch') } = icon("trash-o") - else = link_to project_branch_path(@project, branch.name), class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", - title: "Delete branch", + title: s_('Branches|Delete branch'), method: :delete, - data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" }, + data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } }, remote: true, - "aria-label" => "Delete branch" do + 'aria-label' => s_('Branches|Delete branch') do = icon("trash-o") - if branch.name != @repository.root_ref - .divergence-graph{ title: "#{number_commits_behind} commits behind #{@repository.root_ref}, #{number_commits_ahead} commits ahead" } + .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: number_commits_behind, + default_branch: @repository.root_ref, + number_commits_ahead: number_commits_ahead } } .graph-side .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } %span.count.count-behind= number_commits_behind @@ -76,4 +81,4 @@ = render 'projects/branches/commit', commit: commit, project: @project - else %p - Cant find HEAD commit for this branch + = s_('Branches|Cant find HEAD commit for this branch') diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml index f00a0ee6925..e0008e322a0 100644 --- a/app/views/projects/branches/_delete_protected_modal.html.haml +++ b/app/views/projects/branches/_delete_protected_modal.html.haml @@ -4,36 +4,38 @@ .modal-header %button.close{ data: { dismiss: 'modal' } } × %h3.page-title - Delete protected branch - = surround "'", "'?" do + - title_branch_name = capture do %span.js-branch-name.ref-name>[branch name] + = s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name } .modal-body %p - You’re about to permanently delete the protected branch - = succeed '.' do - %strong.js-branch-name.ref-name [branch name] + - branch_name = capture do + %strong.js-branch-name.ref-name>[branch name] + = s_('Branches|You’re about to permanently delete the protected branch %{branch_name}.').html_safe % { branch_name: branch_name } %p.js-not-merged - default_branch = capture do %span.ref-name= @repository.root_ref - = s_("Branches|This branch hasn’t been merged into %{default_branch}.").html_safe % { default_branch: default_branch } - = s_("Branches|To avoid data loss, consider merging this branch before deleting it.") + = s_('Branches|This branch hasn’t been merged into %{default_branch}.').html_safe % { default_branch: default_branch } + = s_('Branches|To avoid data loss, consider merging this branch before deleting it.') %p - Once you confirm and press - = succeed ',' do - %strong Delete protected branch - it cannot be undone or recovered. + - delete_protected_branch = capture do + %strong + = s_('Branches|Delete protected branch') + = s_('Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered.').html_safe % { delete_protected_branch: delete_protected_branch } %p - %strong To confirm, type - %kbd.js-branch-name [branch name] + - branch_name_confirmation = capture do + %kbd.js-branch-name [branch name] + %strong + = s_('Branches|To confirm, type %{branch_name_confirmation}:').html_safe % { branch_name_confirmation: branch_name_confirmation } .form-group = text_field_tag 'delete_branch_input', '', class: 'form-control js-delete-branch-input' .modal-footer %button.btn{ data: { dismiss: 'modal' } } Cancel - = link_to 'Delete protected branch', '', + = link_to s_('Branches|Delete protected branch'), '', class: "btn btn-danger js-delete-branch", - title: 'Delete branch', + title: s_('Branches|Delete branch'), method: :delete, - "aria-label" => "Delete" + 'aria-label' => s_('Branches|Delete branch') diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 73583c6bbc2..7d9645d79e6 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,17 +1,16 @@ - @no_container = true -- page_title "Branches" -= render "projects/commits/head" +- page_title _('Branches') %div{ class: container_class } .top-area.adjust - if can?(current_user, :admin_project, @project) .nav-text - Protected branches can be managed in - = link_to 'project settings', project_protected_branches_path(@project) + - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project) + = s_('Branches|Protected branches can be managed in %{project_settings_link}').html_safe % { project_settings_link: project_settings_link } .nav-controls = form_tag(filter_branches_path, method: :get) do - = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } + = search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline> %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } @@ -20,16 +19,21 @@ = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header - Sort by + = s_('Branches|Sort by') - branches_sort_options_hash.each do |value, title| %li = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value) - if can? current_user, :push_code, @project - = link_to project_merged_branches_path(@project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do - Delete merged branches + = link_to project_merged_branches_path(@project), + class: 'btn btn-inverted btn-remove has-tooltip', + title: s_("Branches|Delete all branches that are merged into '%{default_branch}'") % { default_branch: @project.repository.root_ref }, + method: :delete, + data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'), + container: 'body' } do + = s_('Branches|Delete merged branches') = link_to new_project_branch_path(@project), class: 'btn btn-create' do - New branch + = s_('Branches|New branch') - if @branches.any? %ul.content-list.all-branches @@ -37,6 +41,7 @@ = render "projects/branches/branch", branch: branch = paginate @branches, theme: 'gitlab' - else - .nothing-here-block No branches to show + .nothing-here-block + = s_('Branches|No branches to show') = render 'projects/branches/delete_protected_modal' diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 9d85e027ac9..fa6f6d0b588 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -11,19 +11,15 @@ #{ _('Source code') } %li = link_to archive_project_repository_path(project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do - %i.fa.fa-download %span= _('Download zip') %li = link_to archive_project_repository_path(project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do - %i.fa.fa-download %span= _('Download tar.gz') %li = link_to archive_project_repository_path(project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do - %i.fa.fa-download %span= _('Download tar.bz2') %li = link_to archive_project_repository_path(project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do - %i.fa.fa-download %span= _('Download tar') - if pipeline && pipeline.latest_builds_with_artifacts.any? @@ -36,6 +32,5 @@ - pipeline.latest_builds_with_artifacts.each do |job| %li = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do - %i.fa.fa-download %span #{s_('DownloadArtifacts|Download')} '#{job.name}' diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index b04d6a1fa5e..2589c53beae 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -11,19 +11,16 @@ - if can_create_issue %li = link_to new_project_issue_path(@project) do - = icon('exclamation-circle fw') #{ _('New issue') } - if merge_project %li = link_to project_new_merge_request_path(merge_project) do - = icon('tasks fw') #{ _('New merge request') } - if can_create_snippet %li = link_to new_project_snippet_path(@project) do - = icon('file-text-o fw') #{ _('New snippet') } - if can_create_issue || merge_project || can_create_snippet @@ -32,20 +29,16 @@ - if can?(current_user, :push_code, @project) %li = link_to project_new_blob_path(@project, @project.default_branch || 'master') do - = icon('file fw') #{ _('New file') } %li = link_to new_project_branch_path(@project) do - = icon('code-fork fw') #{ _('New branch') } %li = link_to new_project_tag_path(@project) do - = icon('tags fw') #{ _('New tag') } - elsif current_user && current_user.already_forked?(@project) %li = link_to project_new_blob_path(@project, @project.default_branch || 'master') do - = icon('file fw') #{ _('New file') } - elsif can?(current_user, :fork_project, @project) %li @@ -55,5 +48,4 @@ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do - = icon('file fw') #{ _('New file') } diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index f45cc7f0f45..f880556a9f7 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -4,12 +4,11 @@ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do = custom_icon('icon_fork') %span= s_('GoToYourFork|Fork') - - elsif !current_user.can_create_project? - = link_to new_project_fork_path(@project), title: _('You have reached your project limit'), class: 'btn has-tooltip disabled' do - = custom_icon('icon_fork') - %span= s_('CreateNewFork|Fork') - else - = link_to new_project_fork_path(@project), class: 'btn' do + - can_create_fork = current_user.can?(:create_fork) + = link_to new_project_fork_path(@project), + class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}", + title: (_('You have reached your project limit') unless can_create_fork) do = custom_icon('icon_fork') %span= s_('CreateNewFork|Fork') .count-with-arrow diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index bef96786b73..49b0b314e1d 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -3,6 +3,8 @@ endpoint: endpoint, "help-page-path" => help_page_path('ci/quick_start/README'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), + "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), + "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), } } - content_for :page_specific_javascripts do diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 717de85c5d2..abb292f8f27 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -6,7 +6,6 @@ - @content_class = limited_container_width - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description -= render "projects/commits/head" .container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml deleted file mode 100644 index e1549baef89..00000000000 --- a/app/views/projects/commits/_head.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_tree_path(@project) do - #{ _('Files') } - - = nav_link(controller: [:commit, :commits]) do - = link_to project_commits_path(@project, current_ref) do - #{ _('Commits') } - - = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do - #{ _('Branches') } - - = nav_link(controller: [:tags, :releases]) do - = link_to project_tags_path(@project) do - #{ _('Tags') } - - = nav_link(path: 'graphs#show') do - = link_to project_graph_path(@project, current_ref) do - #{ _('Contributors') } - - = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, current_ref) do - #{ s_('ProjectNetworkGraph|Graph') } - - = nav_link(controller: :compare) do - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do - #{ _('Compare') } - - = nav_link(path: 'graphs#charts') do - = link_to charts_project_graph_path(@project, current_ref) do - #{ _('Charts') } diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index e873b931683..ef305120525 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -5,9 +5,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") -= content_for :sub_nav do - = render "head" - .js-project-commits-show{ 'data-commits-limit' => @limit } %div{ class: container_class } .tree-holder diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 1ce3ad0c0fd..3ad0166e9cd 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - breadcrumb_title "Compare Revisions" - page_title "Compare" -= render "projects/commits/head" %div{ class: container_class } .sub-header-block diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 7cc42455394..f87f1d476f5 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,7 +1,6 @@ - @no_container = true - add_to_breadcrumbs "Compare Revisions", project_compare_index_path(@project) - page_title "#{params[:from]}...#{params[:to]}" -= render "projects/commits/head" %div{ class: container_class } .sub-header-block.no-bottom-space diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 8d008be5aae..c06e9f323af 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -4,8 +4,6 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('cycle_analytics') -= render "projects/head" - #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data .landing.content-block{ "v-if" => "!isOverviewDialogDismissed" } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index b5aea217384..adc4dcbed33 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,5 +1,8 @@ - environment = local_assigns.fetch(:environment, nil) - file_hash = hexdigest(diff_file.file_path) +- image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image' +- image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha + .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) } .js-file-title.file-title-flex-parent .file-header-content @@ -17,6 +20,9 @@ = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) + - if image_diff && image_replaced + = view_file_button(diff_file.old_content_sha, diff_file.old_path, project, replaced: true) + = view_file_button(diff_file.content_sha, diff_file.file_path, project) = view_on_environment_button(diff_file.content_sha, diff_file.file_path, environment) if environment diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml index 01879556894..6b5233833c6 100644 --- a/app/views/projects/diffs/viewers/_image.html.haml +++ b/app/views/projects/diffs/viewers/_image.html.haml @@ -15,8 +15,7 @@ .two-up.view %span.wrap .frame.deleted - %a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) } - = image_tag(old_blob_raw_path, alt: diff_file.old_path) + = image_tag(old_blob_raw_path, alt: diff_file.old_path) %p.image-info.hide %span.meta-filesize= number_to_human_size(old_blob.size) | @@ -27,8 +26,7 @@ %span.meta-height %span.wrap .frame.added - %a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) } - = image_tag(blob_raw_path, alt: diff_file.new_path) + = image_tag(blob_raw_path, alt: diff_file.new_path) %p.image-info.hide %span.meta-filesize= number_to_human_size(blob.size) | diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0a3045604f4..8ae4fd94146 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -3,8 +3,6 @@ - @content_class = "limit-container-width" unless fluid_layout - expanded = Rails.env.test? -= render "projects/settings/head" - .project-edit-container %section.settings.general-settings .settings-header diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 5e980314307..3f3ce10419f 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -3,7 +3,6 @@ = render partial: 'flash_messages', locals: { project: @project } -= render "projects/head" = render "home_panel" .row-content-block.second-block.center @@ -27,6 +26,8 @@ - if can?(current_user, :push_code, @project) %div{ class: container_class } + - if show_auto_devops_callout?(@project) + = render 'shared/auto_devops_callout' .prepend-top-20 .empty_wrapper %h3.page-title-empty diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml index 3871165763c..d6ff3f729b4 100644 --- a/app/views/projects/environments/edit.html.haml +++ b/app/views/projects/environments/edit.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Edit", @environment.name, "Environments" -= render "projects/pipelines/head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index f7e3733ba0b..1bcc955ddc8 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Environments" -= render "projects/pipelines/head" - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index acc80b49dd0..2e85f608823 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Environments" - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) -= render "projects/pipelines/head" - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index e9e1ad9ef30..e0aedcac5e1 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -4,7 +4,6 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_d3' = webpack_bundle_tag 'monitoring' -= render "projects/pipelines/head" .prometheus-container{ class: container_class } .top-area @@ -16,6 +15,8 @@ #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), + "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started'), + "empty-loading-svg-path": image_path('illustrations/monitoring/loading'), + "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'), "additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json), "has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } } - diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index 88f43a1e7e4..62b08e85e22 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -1,7 +1,6 @@ - @no_container = true - breadcrumb_title "Environments" - page_title 'New Environment' -= render "projects/pipelines/head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index c35d1b5aaee..d7859c9fbeb 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -2,7 +2,6 @@ - add_to_breadcrumbs "Environments", project_environments_path(@project) - breadcrumb_title @environment.name - page_title "Environments" -= render "projects/pipelines/head" %div{ class: container_class } .row.top-area.adjust diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 464135b5ac7..a073a164f11 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Terminal for environment", @environment.name -= render "projects/pipelines/head" - content_for :page_specific_javascripts do = stylesheet_link_tag "xterm/xterm" diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 021575160ea..a3467eb6f05 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -1,5 +1,4 @@ - page_title "Find File", @ref -= render "projects/commits/head" .file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, @options.merge(format: :json)))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @id || @commit.id)) } .nav-block diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 0f36e1a7353..906774a21e3 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -13,7 +13,7 @@ - if @namespaces.present? %label.label-light %span - Click to fork the project to a user or group + Click to fork the project - @namespaces.in_groups_of(6, false) do |group| .row - group.each do |namespace| @@ -29,8 +29,12 @@ .caption = namespace.human_name - else - .fork-thumbnail - = link_to project_forks_path(@project, namespace_key: namespace.id), method: "POST" do + - can_create_project = current_user.can?(:create_projects, namespace) + .fork-thumbnail{ class: ("disabled" unless can_create_project) } + = link_to project_forks_path(@project, namespace_key: namespace.id), + method: "POST", + class: ("disabled has-tooltip" unless can_create_project), + title: (_('You have reached your project limit') unless can_create_project) do - if /no_((\w*)_)*avatar/.match(avatar) .no-avatar = icon 'question' diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index f0ef647ddb3..ffb9238a65a 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -4,7 +4,6 @@ = webpack_bundle_tag('common_d3') = webpack_bundle_tag('graphs') = webpack_bundle_tag('graphs_charts') -= render "projects/commits/head" .repo-charts{ class: container_class } %h4.sub-header diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 08b38428b50..70156c03e3c 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -5,8 +5,6 @@ = webpack_bundle_tag('graphs') = webpack_bundle_tag('graphs_show') -= render 'projects/commits/head' - .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .sub-header-block .tree-ref-holder diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml index ab5a7b117d7..1cf4105bd27 100644 --- a/app/views/projects/hook_logs/show.html.haml +++ b/app/views/projects/hook_logs/show.html.haml @@ -1,5 +1,3 @@ -= render 'projects/settings/head' - .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml index c8c17d2d828..b1219f019d7 100644 --- a/app/views/projects/hooks/edit.html.haml +++ b/app/views/projects/hooks/edit.html.haml @@ -1,5 +1,4 @@ - page_title 'Integrations' -= render 'projects/settings/head' .row.prepend-top-default .col-lg-3 @@ -19,4 +18,3 @@ %hr = render partial: 'projects/hook_logs/index', locals: { hook: @hook, hook_logs: @hook_logs, project: @project } - diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml deleted file mode 100644 index e9f21594a71..00000000000 --- a/app/views/projects/issues/_head.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), title: 'Issues' do - %span - List - - = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: 'Board' do - %span - Board - - - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) - = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests' do - %span - Merge Requests - - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do - %span - Labels - - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do - %span - Milestones diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 7dc35be57a6..64c648f201b 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -24,7 +24,7 @@ - if issue.milestone %span.issuable-milestone.hidden-xs - = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title) do + = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(issue.milestone) } do = icon('clock-o') = issue.milestone.title - if issue.due_date diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index e72c94695bc..bfaf024428d 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -3,8 +3,6 @@ - page_title "Issues" - new_issue_email = @project.new_issue_address(current_user) -= content_for :sub_nav do - = render "projects/issues/head" - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index 8604c7d3ea4..4a238b99b58 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Jobs" -= render "projects/pipelines/head" %div{ class: container_class } .top-area diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 975c08c06e6..ce0e3872240 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -2,7 +2,6 @@ - add_to_breadcrumbs "Jobs", project_jobs_path(@project) - breadcrumb_title "##{@build.id}" - page_title "#{@build.name} (##{@build.id})", "Jobs" -= render "projects/pipelines/head" %div{ class: container_class } .build-page.js-build-page diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 84b0b65d1c0..b8ee4305142 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Edit", @label.name, "Labels" -= render "shared/mr_head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 10d07ce8e45..80e4dce1a80 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,8 +3,6 @@ - hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) -= render "shared/mr_head" - - if @labels.exists? || @prioritized_labels.exists? %div{ class: container_class } .top-area.adjust diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 562b6fb8d8c..02f59f30a39 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,7 +1,6 @@ - @no_container = true - breadcrumb_title "Labels" - page_title "New Label" -= render "shared/mr_head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml deleted file mode 100644 index 1e505222887..00000000000 --- a/app/views/projects/merge_requests/_head.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests' do - %span - List - - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do - %span - Labels - - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do - %span - Milestones diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 0a1ebcb8124..2b5e8711b0a 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -23,7 +23,7 @@ - if merge_request.milestone %span.issuable-milestone.hidden-xs - = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title) do + = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(merge_request.milestone) } do = icon('clock-o') = merge_request.milestone.title - if merge_request.target_project.default_branch != merge_request.target_branch diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 2c53891a92d..6b8dcb3e60b 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -4,9 +4,6 @@ - new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project - page_title "Merge Requests" -- unless @project.issues_enabled? - = content_for :sub_nav do - = render "projects/merge_requests/head" - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 1e66c6079e3..af3f25c6a30 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "Edit", @milestone.title, "Milestones" -= render "shared/mr_head" %div{ class: container_class } diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index f3abecdd302..fcbf7cb802b 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,8 +1,6 @@ - @no_container = true - page_title 'Milestones' -= render "shared/mr_head" - %div{ class: container_class } .top-area = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 84ffbc0a926..c301f517013 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,7 +1,6 @@ - @no_container = true - breadcrumb_title "Milestones" - page_title "New Milestone" -= render "shared/mr_head" %div{ class: container_class } %h3.page-title diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 1f5f18801ad..a5153df1159 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -3,7 +3,6 @@ - breadcrumb_title @milestone.title - page_title @milestone.title, "Milestones" - page_description @milestone.description -= render "shared/mr_head" %div{ class: container_class } .detail-page-header.milestone-page-header diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index e29cb277389..8a19497c55b 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -2,7 +2,6 @@ - page_title "Graph", @ref - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('network') -= render "projects/commits/head" = render "head" %div{ class: container_class } .project-network diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index de76832331a..4961835f12a 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -1,7 +1,8 @@ +- access = note_max_access_for_user(note) - if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR) - %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project. Handle with care.") } + %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") } = issuable_first_contribution_icon -- if access = note_max_access_for_user(note) +- if access.nonzero? %span.note-role.note-role-access= Gitlab::Access.human_access(access) - if note.resolvable? diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 098b0ef56ef..04e647c0dc6 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,5 +1,4 @@ - page_title 'Pages' -= render "projects/settings/head" %h3.page_title Pages diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 2b081786b6a..4fbdd1dd5e4 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -7,8 +7,6 @@ - @no_container = true - page_title _("Pipeline Schedules") -= render "projects/pipelines/head" - %div{ class: container_class } #pipeline-schedules-callout{ data: { docs_url: help_page_path('user/project/pipelines/schedules') } } .top-area diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml deleted file mode 100644 index ee2f236cec4..00000000000 --- a/app/views/projects/pipelines/_head.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - - 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_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do - %span - Charts diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index 487ac87186d..ba55bc23add 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -4,7 +4,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') -= render 'head' %div{ class: container_class } .sub-header-block diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 9fc15d20f34..a10a7c23924 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -1,14 +1,14 @@ - @no_container = true - page_title "Pipelines" -= render "projects/pipelines/head" %div{ 'class' => container_class } - if show_auto_devops_callout?(@project) = render 'shared/auto_devops_callout' - #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), "help-page-path" => help_page_path('ci/quick_start/README'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), + "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), + "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "new-pipeline-path" => new_project_pipeline_path(@project), "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "all-path" => project_pipelines_path(@project), @@ -17,7 +17,7 @@ "finished-path" => project_pipelines_path(@project, scope: :finished), "branches-path" => project_pipelines_path(@project, scope: :branches), "tags-path" => project_pipelines_path(@project, scope: :tags), - "has-ci" => @project.has_ci?, + "has-ci" => @repository.gitlab_ci_yml, "ci-lint-path" => ci_lint_path } } = page_specific_javascript_bundle_tag('common_vue') diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 7cc9fe79afd..2174154b207 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -2,7 +2,6 @@ - add_to_breadcrumbs "Pipelines", project_pipelines_path(@project) - breadcrumb_title "##{@pipeline.id}" - page_title "Pipeline" -= render "projects/pipelines/head" .js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } } - if @commit diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 2aceb4b529c..77211099830 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -3,11 +3,15 @@ = form_for @project, url: project_pipelines_settings_path(@project) do |f| %fieldset.builds-feature .form-group - %p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery. %h5 Auto DevOps (Beta) %p - Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration. + Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration. + This will happen starting with the next event (e.g.: push) that occurs to the project. = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md') + - message = auto_devops_warning_message(@project) + - if message + %p.settings-message.text-center + = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| .radio = form.label :enabled_true do @@ -15,26 +19,25 @@ %strong Enable Auto DevOps %br %span.descr - The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml - in the project. + The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project. .radio = form.label :enabled_false do = form.radio_button :enabled, 'false' %strong Disable Auto DevOps %br %span.descr - A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery. + An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery. + .radio = form.label :enabled_nil do = form.radio_button :enabled, '' - %strong - Instance default (status: #{current_application_settings.auto_devops_enabled?}) + %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'}) %br %span.descr - Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified. + Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>. %br %p - Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed. + You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages. = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' %hr diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index c786298e341..4d962f9433f 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -2,7 +2,6 @@ - add_to_breadcrumbs "Tags", project_tags_path(@project) - breadcrumb_title @tag.name - page_title "Edit", @tag.name, "Tags" -= render "projects/commits/head" %div{ class: container_class } .sub-header-block.no-bottom-space diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml index 3e2a24a4c32..25770df1c90 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/services/edit.html.haml @@ -2,5 +2,4 @@ - page_title @service.title, "Services" - add_to_breadcrumbs("Settings", edit_project_path(@project)) -= render "projects/settings/head" = render 'form' diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml deleted file mode 100644 index 7d24c6a9122..00000000000 --- a/app/views/projects/settings/_head.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: container_class } - - 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 - - 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 project_settings_repository_path(@project), title: 'Repository' do - %span - Repository - - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do - %span - Pipelines - - if @project.pages_available? - = nav_link(controller: :pages) do - = link_to project_pages_path(@project), title: 'Pages' do - %span - Pages diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index d4f71d023c6..62455d0d40d 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -2,8 +2,6 @@ - page_title "CI / CD Settings" - page_title "CI / CD" -= render "projects/settings/head" - - expanded = Rails.env.test? %section.settings#js-general-pipeline-settings @@ -13,7 +11,7 @@ %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p - Update your CI/CD configuration, like job timeout. + Update your CI/CD configuration, like job timeout or Auto DevOps. .settings-content.no-animate{ class: ('expanded' if expanded) } = render 'projects/pipelines_settings/show' diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index 933daa7f549..2f1a548e119 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,6 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout - breadcrumb_title "Integrations Settings" - page_title 'Integrations' -= render "projects/settings/head" = render 'projects/hooks/index' = render 'projects/services/index' diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml index 1e7695ac397..ea2cd36b212 100644 --- a/app/views/projects/settings/members/show.html.haml +++ b/app/views/projects/settings/members/show.html.haml @@ -1,6 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout - page_title "Members" -= render "projects/settings/head" = render "projects/project_members/index" diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 6d4af72b8ea..517d51993d2 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -2,8 +2,6 @@ - page_title "Repository" - @content_class = "limit-container-width" unless fluid_layout -= render "projects/settings/head" - - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('deploy_keys') diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index d8f5114f4b5..705a4607ad2 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,7 +7,6 @@ = render partial: 'flash_messages', locals: { project: @project } -= render "projects/head" = render "projects/last_push" = render "home_panel" diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index a6fe02fcae0..27d58d4c0e8 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -2,7 +2,6 @@ - @sort ||= sort_value_recently_updated - page_title "Tags" - add_to_breadcrumbs("Repository", project_tree_path(@project)) -= render "projects/commits/head" .flex-list{ class: container_class } .top-area.adjust diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 5d6eb4f4026..43aa2b27af6 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -2,7 +2,6 @@ - add_to_breadcrumbs "Tags", project_tags_path(@project) - breadcrumb_title @tag.name - page_title @tag.name, "Tags" -= render "projects/commits/head" %div{ class: container_class } .top-area.multi-line diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml index 13705ca303b..3a43dde8052 100644 --- a/app/views/projects/tree/_old_tree_header.html.haml +++ b/app/views/projects/tree/_old_tree_header.html.haml @@ -20,15 +20,12 @@ - if can_edit_tree? %li = link_to project_new_blob_path(@project, @id) do - = icon('pencil fw') #{ _('New file') } %li = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do - = icon('file fw') #{ _('Upload file') } %li = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do - = icon('folder fw') #{ _('New directory') } - elsif can?(current_user, :fork_project, @project) %li @@ -38,7 +35,6 @@ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do - = icon('pencil fw') #{ _('New file') } %li - continue_params = { to: request.fullpath, @@ -47,7 +43,6 @@ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do - = icon('file fw') #{ _('Upload file') } %li - continue_params = { to: request.fullpath, @@ -56,15 +51,12 @@ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do - = icon('folder fw') #{ _('New directory') } %li.divider %li = link_to new_project_branch_path(@project) do - = icon('code-fork fw') #{ _('New branch') } %li = link_to new_project_tag_path(@project) do - = icon('tags fw') #{ _('New tag') } diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 853e2a6e7ec..6cddc38d11a 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,10 +1,6 @@ .tree-ref-container .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path - - if show_new_repo? - .tree-ref-target-holder.js-tree-ref-target-holder - = icon('long-arrow-right', title: 'to target branch') - = render 'shared/target_switcher', destination: 'tree', path: @path - unless show_new_repo? = render 'projects/tree/old_tree_header' diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index d84a1fd7ee1..f819f2addaa 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -11,8 +11,6 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'repo' -= render "projects/commits/head" - %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - if show_auto_devops_callout?(@project) = render 'shared/auto_devops_callout' diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index e5a1fccf9ba..4e265bf733a 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,4 +1,5 @@ -- commit_message = @page.persisted? ? "Update #{@page.title}" : "Create #{@page.title}" +- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") +- commit_message = commit_message % { page_title: @page.title } = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) @@ -12,13 +13,13 @@ .form-group .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 - = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control" + = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control' .form-group .col-sm-12= f.label :content, class: 'control-label-full-width' .col-sm-12 = render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do - = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here...") = render 'shared/notes/hints' .clearfix @@ -26,12 +27,11 @@ .help-block = succeed '.' do - To link to a (new) page, simply type - %code [Link Title](page-slug) + = (s_("WikiMarkdownTip|To link to a (new) page, simply type %{link_example}") % { link_example: '<code>[Link Title](page-slug)</code>' }).html_safe = succeed '.' do - More examples are in the - = link_to 'documentation', help_page_path("user/markdown", anchor: "wiki-specific-markdown") + - markdown_link = link_to s_("WikiMarkdownDocs|documentation"), help_page_path('user/markdown', anchor: 'wiki-specific-markdown') + = (s_("WikiMarkdownDocs|More examples are in the %{docs_link}") % { docs_link: markdown_link }).html_safe .form-group .col-sm-12= f.label :commit_message, class: 'control-label-full-width' @@ -39,10 +39,10 @@ .form-actions - if @page && @page.persisted? - = f.submit 'Save changes', class: "btn-save btn" + = f.submit _("Save changes"), class: 'btn-save btn' .pull-right - = link_to "Cancel", project_wiki_path(@project, @page), class: "btn btn-cancel btn-grouped" + = link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped' - else - = f.submit 'Create page', class: "btn-create btn" + = f.submit s_("Wiki|Create page"), class: 'btn-create btn' .pull-right - = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel" + = link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel' diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 3bbd8042c3a..cadda0a33c2 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,9 +1,9 @@ - if (@page && @page.persisted?) - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New page + = s_("Wiki|New page") = link_to project_wiki_history_path(@project, @page), class: "btn" do - Page history + = s_("Wiki|Page history") - if can?(current_user, :create_wiki, @project) && @page.latest? = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit" do - Edit + = _("Edit") diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 13dd8461433..06a3cac12d5 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -3,16 +3,15 @@ .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3.page-title New Wiki Page + %h3.page-title= s_("WikiNewPageTitle|New Wiki Page") .modal-body %form.new-wiki-page .form-group = label_tag :new_wiki_path do - %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true + %span= s_("WikiPage|Page slug") + = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true %span.new-wiki-page-slug-tip = icon('lightbulb-o') - Tip: You can specify the full path for the new file. - We will automatically create any missing directories. + = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") .form-actions - = button_tag 'Create page', class: 'build-new-wiki btn btn-create' + = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-create" diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml index 7c2f562d422..0a1ccbc5f1c 100644 --- a/app/views/projects/wikis/_pages_wiki_page.html.haml +++ b/app/views/projects/wikis/_pages_wiki_page.html.haml @@ -2,4 +2,4 @@ = link_to wiki_page.title, project_wiki_path(@project, wiki_page) %small (#{wiki_page.format}) .pull-right - %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} + %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index f7283ae4739..5b781294d68 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -8,7 +8,7 @@ = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do = succeed ' ' do = icon('cloud-download') - Clone repository + = _("Clone repository") .blocks-container .block.block-first @@ -17,6 +17,6 @@ .block = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do - More Pages + = s_("Wiki|More Pages") = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 8fd60216536..0d77e5bd16d 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,11 +1,10 @@ - @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout -- page_title "Edit", @page.title.capitalize, "Wiki" +- page_title _("Edit"), @page.title.capitalize, _("Wiki") - if @conflict .alert.alert-danger - Someone edited the page the same time you did. Please check out - = link_to "the page", project_wiki_path(@project, @page), target: "_blank" - and make sure your changes will not unintentionally remove theirs. + - page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank" + = (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } @@ -20,20 +19,20 @@ %span.light · - if @page.persisted? - Edit Page + = s_("Wiki|Edit Page") - else - Create Page + = s_("Wiki|Create Page") .nav-controls - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New page + = s_("Wiki|New page") - if @page.persisted? = link_to project_wiki_history_path(@project, @page), class: "btn" do - Page history + = s_("Wiki|Page history") - if can?(current_user, :admin_wiki, @project) - = link_to project_wiki_path(@project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do - Delete + = link_to project_wiki_path(@project, @page), data: { confirm: s_("WikiPageConfirmDelete|Are you sure you want to delete this page?")}, method: :delete, class: "btn btn-danger" do + = _("Delete") = render 'form' diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml index 7dfa405d063..911e1339541 100644 --- a/app/views/projects/wikis/empty.html.haml +++ b/app/views/projects/wikis/empty.html.haml @@ -1,6 +1,6 @@ -- page_title "Wiki" +- page_title _("Wiki") -%h3.page-title Empty page +%h3.page-title= _("Wiki|Empty page") %hr .error_message - You are not allowed to create wiki pages + = s_("WikiEmptyPageError|You are not allowed to create wiki pages") diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index e740fb93ea4..10dbbc0e42c 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,36 +1,34 @@ - @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout -- page_title "Git Access", "Wiki" +- page_title s_("WikiClone|Git Access"), _("Wiki") .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') .git-access-header - Clone repository + = _("Clone repository") %strong= @project_wiki.full_path = render "shared/clone_panel", project: @project_wiki .wiki-git-access - %h3 Install Gollum + %h3= s_("WikiClone|Install Gollum") %pre.dark :preserve gem install gollum %p - It is recommended to install - %code github-markdown - so that GFM features render locally: + = (s_("WikiClone|It is recommended to install %{markdown} so that GFM features render locally:") % { markdown: "<code>github-markdown</code>" }).html_safe %pre.dark :preserve gem install github-markdown - %h3 Clone your wiki + %h3= s_("WikiClone|Clone your wiki") %pre.dark :preserve git clone #{ content_tag(:span, h(default_url_to_repo(@project_wiki)), class: 'clone')} cd #{h @project_wiki.path} - %h3 Start Gollum and edit locally + %h3= s_("WikiClone|Start Gollum and edit locally") %pre.dark :preserve gollum diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 306feeff259..bc1ab5065e4 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,4 +1,4 @@ -- page_title "History", @page.title.capitalize, "Wiki" +- page_title _("History"), @page.title.capitalize, _("Wiki") .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } @@ -9,17 +9,17 @@ = link_to @page.title.capitalize, project_wiki_path(@project, @page) %span.light · - History + = _("History") .table-holder %table.table %thead %tr - %th Page version - %th Author - %th Commit Message - %th Last updated - %th Format + %th= s_("Wiki|Page version") + %th= _("Author") + %th= _("Commit Message") + %th= _("Last updated") + %th= _("Format") %tbody - @page.versions.each_with_index do |version, index| - commit = version diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index d533c611a38..aeef64fd7eb 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,19 +1,19 @@ - @no_container = true - add_to_breadcrumbs "Wiki", get_project_wiki_path(@project) -- breadcrumb_title "Pages" -- page_title "Pages", "Wiki" +- breadcrumb_title s_("Wiki|Pages") +- page_title s_("Wiki|Pages"), _("Wiki") %div{ class: container_class } .wiki-page-header .nav-text %h2.wiki-page-title - Wiki Pages + = s_("Wiki|Wiki Pages") .nav-controls = link_to project_wikis_git_access_path(@project), class: 'btn' do = icon('cloud-download') - Clone repository + = _("Clone repository") %ul.wiki-pages-list.content-list = render @wiki_entries, context: 'pages' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index b066a812ec8..62c18cc4582 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,8 +1,8 @@ - @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout - breadcrumb_title @page.title.capitalize - wiki_breadcrumb_dropdown_links(@page.slug) -- page_title @page.title.capitalize, "Wiki" -- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project) +- page_title @page.title.capitalize, _("Wiki") +- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project) .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } @@ -11,9 +11,7 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - Last edited by - %strong - #{@page.commit.author.name} + = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author.name}</strong>" }).html_safe #{time_ago_with_tooltip(@page.commit.authored_date)} .nav-controls @@ -21,8 +19,10 @@ - if @page.historical? .warning_message - This is an old version of this page. - You can view the #{link_to "most recent version", project_wiki_path(@project, @page)} or browse the #{link_to "history", project_wiki_history_path(@project, @page)}. + = s_("WikiHistoricalPage|This is an old version of this page.") + - most_recent_link = link_to s_("WikiHistoricalPage|most recent version"), project_wiki_path(@project, @page) + - history_link = link_to s_("WikiHistoricalPage|history"), project_wiki_history_path(@project, @page) + = (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe .wiki-holder.prepend-top-default.append-bottom-default .wiki diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index 2f09c2fec87..7c633175a06 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -6,10 +6,10 @@ .svg-container = custom_icon('icon_autodevops') .user-callout-copy - %h4= _('Auto DevOps (Beta)') - %p= _('Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') + %h4= s_('AutoDevOps|Auto DevOps (Beta)') + %p= s_('AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p - #{s_('AutoDevOps|Learn more in the')} - = link_to _('Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer' + - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') + = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } - = link_to _('Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout' + = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout' diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 2e1bd5a088c..d0b9e891b82 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -22,11 +22,9 @@ - if @group.persisted? .alert.alert-warning.prepend-top-10 - %ul - %li Changing group path can have unintended side effects. - %li Renaming group path will rename directory for all related projects - %li It will change web url for access group and group projects. - %li It will change the git path to repositories under this group. + Changing group path can have unintended side effects. + = succeed '.' do + = link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank' .form-group.group-name-holder = f.label :name, class: 'control-label' do diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml deleted file mode 100644 index e7355ae2eea..00000000000 --- a/app/views/shared/_mr_head.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if @project.issues_enabled? - = render "projects/issues/head" -- else - = render "projects/merge_requests/head" diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml deleted file mode 100644 index 61646f150c1..00000000000 --- a/app/views/shared/_nav_scroll.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.fade-left - = icon('angle-left') -.fade-right - = icon('angle-right') diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml index eb5ddb0dde4..2530db986e0 100644 --- a/app/views/shared/_sidebar_toggle_button.html.haml +++ b/app/views/shared/_sidebar_toggle_button.html.haml @@ -1,8 +1,8 @@ %a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" } - = icon('angle-double-left') - = icon('angle-double-right') + = sprite_icon('angle-double-left', css_class: 'icon-angle-double-left') + = sprite_icon('angle-double-right', css_class: 'icon-angle-double-right') %span.collapse-text Collapse sidebar = button_tag class: 'close-nav-button', type: 'button' do - = icon ('times') + = sprite_icon('close', size: 16) %span.collapse-text Close sidebar diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 785a500e44e..7ff5e679f17 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,36 +1,16 @@ +- sorted_by = sort_options_hash[@sort] - 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? - = sort_options_hash[@sort] - - else - = sort_title_recently_created + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } + = sorted_by = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-sort %li - = link_to page_filter_path(sort: sort_value_priority, label: true) do - = sort_title_priority - = link_to page_filter_path(sort: sort_value_label_priority, label: true) do - = sort_title_label_priority - = link_to page_filter_path(sort: sort_value_recently_created, label: true) do - = sort_title_recently_created - = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do - = sort_title_oldest_created - = link_to page_filter_path(sort: sort_value_recently_updated, label: true) do - = sort_title_recently_updated - = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do - = sort_title_oldest_updated - = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do - = sort_title_milestone_soon - = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do - = sort_title_milestone_later - - 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 - = sort_title_due_date_later - = link_to page_filter_path(sort: sort_value_upvotes, label: true) do - = sort_title_upvotes - = link_to page_filter_path(sort: sort_value_downvotes, label: true) do - = sort_title_downvotes + = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by) + = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) + = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by) + = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sorted_by) + = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sorted_by) if viewing_issues + = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sorted_by) + = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sorted_by) diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml deleted file mode 100644 index bbe9692a7da..00000000000 --- a/app/views/shared/_target_switcher.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- dropdown_toggle_text = @ref || @project.default_branch -= form_tag nil, method: :get, class: "project-refs-form project-refs-target-form" do - = hidden_field_tag :destination, destination - - if defined?(path) - = hidden_field_tag :path, path - - @options && @options.each do |key, value| - = hidden_field_tag key, value, id: nil - .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, find: ['branches']), field_name: 'ref', input_field_name: 'new-branch', submit_form_on_click: true, visit: false }, { toggle_class: "js-project-refs-dropdown" } - .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } - = dropdown_title _("Create a new branch") - = dropdown_input _("Create a new branch") - = dropdown_title _("Select existing branch"), options: {close: false} - = dropdown_filter _("Search branches and tags") - = dropdown_content - = dropdown_loading diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml deleted file mode 100644 index 17ffcba69d8..00000000000 --- a/app/views/shared/_user_callout.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.user-callout{ data: { uid: 'user_callout_dismissed' } } - .bordered-box.landing.content-block - %button.btn.btn-default.close.js-close-callout{ type: 'button', - 'aria-label' => 'Dismiss customize experience box' } - = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') - .svg-container - = custom_icon('icon_customization') - .user-callout-copy - %h4 - Customize your experience - %p - Change syntax themes, default project pages, and more in preferences. - = link_to 'Check it out', profile_preferences_path, class: 'btn btn-primary js-close-callout' diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 1a50b7d4b69..ee8ad8e3999 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -12,13 +12,11 @@ %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal -= render "projects/issues/head" +#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" } + .hidden-xs.hidden-sm + = render 'shared/issuable/search_bar', type: :boards -.hidden-xs.hidden-sm - = render 'shared/issuable/search_bar', type: :boards - -#board-app.boards-app{ "v-cloak" => true, data: board_data } - .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } + .boards-list .boards-app-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") %board{ "v-cloak" => true, @@ -31,10 +29,11 @@ ":board-id" => "boardId", ":key" => "_uid" } = render "shared/boards/components/sidebar" - %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), - "new-issue-path" => new_project_issue_path(@project), - "milestone-path" => milestones_filter_dropdown_path, - "label-path" => labels_filter_path, - ":issue-link-base" => "issueLinkBase", - ":root-path" => "rootPath", - ":project-id" => @project.try(:id) } + - if @project + %board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project), + "milestone-path" => milestones_filter_dropdown_path, + "label-path" => labels_filter_path, + "empty-state-svg" => image_path('illustrations/issues.svg'), + ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", + ":project-id" => @project.id } diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 68737e8da66..de26fa8bbf3 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -5,7 +5,7 @@ .row.empty-state .col-xs-12 .svg-content - = render 'shared/empty_states/icons/issues.svg' + = image_tag 'illustrations/issues.svg' .col-xs-12.text-center .text-content - if has_button && current_user diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index bfda522f2f6..a65634dce53 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -1,7 +1,7 @@ .row.empty-state.labels .col-xs-12 .svg-content - = render 'shared/empty_states/icons/labels.svg' + = image_tag 'illustrations/labels.svg' .col-xs-12.text-center .text-content %h4 Labels can be applied to issues and merge requests to categorize them. diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index ff5741b6d61..67f906903e9 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -5,7 +5,7 @@ .row.empty-state.merge-requests .col-xs-12 .svg-content - = render 'shared/empty_states/icons/merge_requests.svg' + = image_tag 'illustrations/merge_requests.svg' .col-xs-12.text-center .text-content - if has_button diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml index bc268301a97..555cb4f4af9 100644 --- a/app/views/shared/empty_states/_priority_labels.html.haml +++ b/app/views/shared/empty_states/_priority_labels.html.haml @@ -1,3 +1,4 @@ .text-center - = render 'shared/empty_states/icons/priority_labels.svg' + .svg-content + = image_tag 'illustrations/priority_labels.svg' %p Star labels to start sorting by priority diff --git a/app/views/shared/empty_states/icons/_issues.svg b/app/views/shared/empty_states/icons/_issues.svg deleted file mode 100644 index 2e92bf19579..00000000000 --- a/app/views/shared/empty_states/icons/_issues.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.9423" x="25" y="88.4231" rx="2"/><mask id="h" width="25" height="8.9423" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.8013h43V91.404H16z"/><mask id="i" width="43" height="61.6026" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5747 24.863c.1564 1.0866-.253 1.2572-.912.384L66 86.436l-9-6.9552"/><mask id="j" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.2496 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5748 24.863c.1562 1.0866-.2532 1.2572-.9123.384L9.2495 86.436l-9-6.9552"/><mask id="k" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.8013L35.786 1.4556c.9466-1.3562 2.4792-1.3594 3.428 0L59 29.8013"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.2653" height="35.5088" x="6.3673" rx="13.1327"/><mask id="m" width="26.2653" height="35.5088" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.8367" height="22.386" x="4.0816" rx="8.4184"/><mask id="n" width="16.8367" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792.000000, 255.000000)"><g fill="#FDE5D8"><path d="M225.4372 59.5866c-.059.5897-.1323 1.2698-.2203 2.0305-.252 2.1764-.5717 4.559-.9653 7.07-.1283.8185.4312 1.586 1.2496 1.7143.8185.1283 1.586-.4312 1.7142-1.2497.4-2.5528.7253-4.975.9815-7.1898.0898-.7762.1646-1.4715.2252-2.0762.0366-.365.0604-.62.0722-.7557.0717-.8254-.539-1.5526-1.3645-1.6244-.8254-.0717-1.5526.539-1.6244 1.3645-.0106.1228-.0332.365-.0684.7166zM219.8738 87.9413c-.2563.7878.1745 1.6342.9622 1.8906.7878.2562 1.6342-.1745 1.8906-.9623.975-2.9962 1.849-6.2827 2.6287-9.797.1794-.8086-.3308-1.6097-1.1395-1.789-.8088-.1795-1.61.3306-1.7893 1.1394-.76 3.4256-1.6096 6.6206-2.5527 9.5183zM209.9266 103.166c-.781.2766-1.1897 1.134-.913 1.9148.2765.781 1.1338 1.1897 1.9147.913 2.9792-1.0552 5.5414-3.679 7.7796-7.6272.4084-.7207.1554-1.636-.5653-2.0447-.7207-.4086-1.636-.1556-2.0446.565-1.9152 3.3786-3.9945 5.508-6.1714 6.279zM190.439 107.5834c-.7636.3214-1.122 1.201-.8005 1.9645.3215.7634 1.201 1.1217 1.9645.8003 3.1204-1.314 6.2717-2.3243 9.258-2.9816.809-.178 1.3205-.9783 1.1424-1.7874-.178-.809-.9783-1.3205-1.7874-1.1424-3.1666.697-6.4914 1.763-9.777 3.1464zM173.231 118.6257c-.6005.5706-.6248 1.52-.0542 2.1206s1.52.625 2.1206.0543c2.282-2.1682 4.8656-4.162 7.6758-5.946.6994-.444.9064-1.371.4624-2.0704-.444-.6994-1.371-.9064-2.0704-.4624-2.9698 1.8854-5.707 3.998-8.1342 6.304zM162.4543 136.2492c-.2022.8034.2852 1.6185 1.0885 1.8207.8034.202 1.6186-.2853 1.8208-1.0886.7688-3.0547 2.0416-5.9768 3.781-8.7486.4403-.7018.2284-1.6276-.4733-2.068-.7017-.4402-1.6275-.2283-2.068.4734-1.9026 3.0322-3.3016 6.2438-4.149 9.611zM162.1894 156.693c.1036.822.854 1.4042 1.676 1.3006.8218-.1037 1.404-.854 1.3004-1.676-.367-2.9097-.5796-6.1364-.6444-9.8167-.0146-.8284-.698-1.488-1.5262-1.4734-.8283.0146-1.488.698-1.4733 1.5262.0665 3.783.286 7.1162.6674 10.1393zM168.408 176.1653c.3876.7322 1.2953 1.0117 2.0275.6242.7322-.3875 1.0117-1.2952.6242-2.0274-1.6733-3.162-2.9028-5.9954-3.8477-8.943-.2528-.789-1.0973-1.2235-1.8862-.9706-.789.2528-1.2234 1.0974-.9706 1.8863 1.0025 3.1275 2.3014 6.121 4.053 9.4306zM175.9738 188.9357c1.056 1.7165 1.8892 3.0806 2.7307 4.474.4283.709 1.3503.9368 2.0595.5085.709-.4283.9368-1.3503.5085-2.0595-.8464-1.4014-1.6836-2.772-2.7434-4.4948.0808.131-1.9545-3.1733-2.486-4.0405-.4328-.7063-1.3563-.928-2.0627-.495-.7063.4327-.928 1.3563-.495 2.0626.5334.8707 2.5708 4.1785 2.4885 4.0447zM184.83 211.3822c.011.8284.6912 1.491 1.5196 1.4803.8283-.0108 1.491-.691 1.4803-1.5194-.046-3.519-.6604-6.996-1.8367-10.3262-.276-.7812-1.1328-1.1908-1.914-.915-.781.276-1.1906 1.133-.9147 1.914 1.0668 3.0206 1.624 6.1733 1.6655 9.3664zM179.3467 229.4095c-.459.6896-.2723 1.6208.4173 2.08.6896.459 1.6208.272 2.08-.4175 1.966-2.9533 3.4756-6.124 4.4877-9.4165.2434-.7918-.2012-1.631-.993-1.8745-.792-.2434-1.6312.2012-1.8746.993-.9264 3.014-2.3108 5.922-4.1173 8.6355z"/></g><g transform="translate(336.866969, 147.225953) rotate(-300.000000) translate(-336.866969, -147.225953) translate(299.366969, 69.725953)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.6603m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.3526" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="translate(9.124810, 78.967887) scale(-1, 1) translate(-9.124810, -78.967887)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="34.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="40.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="46.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.4327"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.4647"/></g><path fill="#EEE" d="M96.0426 37.2106c-.1512 1.6874.0814 3.815.997 6.146.2046.5207.7936.7774 1.3155.5733.522-.2043.7793-.792.5747-1.313-.7912-2.0142-.99-3.832-.865-5.226.0102-.1143.0195-.186.0238-.2113.092-.552-.2814-1.0738-.8344-1.1658-.553-.092-1.076.2808-1.168.8326-.0126.075-.0285.1975-.0434.364zM107.5302 52.8934c.4913.239 1.098.0626 1.355-.394.2572-.4566.0674-1.0205-.4238-1.2595-1.8668-.9083-3.4584-1.9152-4.7943-3.0075-.4162-.3404-1.0506-.3026-1.4168.0843-.3663.387-.3256.9766.0907 1.317 1.4583 1.1925 3.1828 2.2835 5.1893 3.2596zM120.661 58.9533c.5467.171 1.1257-.1425 1.2933-.7003.1675-.5577-.1397-1.1484-.6864-1.3194-3.0283-.9472-4.1984-1.3178-5.915-1.8824-.544-.179-1.1274.126-1.3028.6813-.1754.5552.1235 1.1504.6677 1.3294 1.729.5686 2.9053.941 5.943 1.8913zM132.5954 62.881c.449.246 1.022.0983 1.2798-.33.258-.4282.103-.975-.3458-1.221-1.4942-.819-3.1928-1.545-5.2675-2.2746-.486-.1708-1.025.0664-1.204.53-.179.4634.0697.9776.5555 1.1484 1.9832.6973 3.5892 1.3838 4.982 2.1472zM141.9774 73.383c.205.4938.809.742 1.3485.5543.5395-.1878.8106-.7404.6055-1.2344-.8504-2.0482-1.853-3.7962-3.0375-5.3046-.337-.429-.99-.527-1.4588-.2184-.4687.3085-.5755.9064-.2386 1.3354 1.0743 1.368 1.9926 2.9692 2.7808 4.8675zM144.609 87.025c.0183.5535.5682.99 1.2283.9746.66-.0153 1.1805-.4764 1.1622-1.03-.0725-2.2033-.2693-4.206-.622-6.1198-.1008-.5473-.7115-.9225-1.3642-.838-.6526.0846-1.1.597-.999 1.1442.336 1.8248.5248 3.745.5947 5.869z"/><path fill="#E5E5E5" d="M144.1423 95.7297c-.0863 2.5442-.1214 3.769-.1422 5.2548-.0076.5523.3963 1.007.9022 1.0154.506.0083.9223-.4326.93-.985.0205-1.4668.0554-2.6812.1412-5.2113l.026-.7667c.0185-.552-.3764-1.016-.882-1.0363-.5056-.0203-.9306.411-.949.963l-.026.766zM144.939 115.201c.1196.5447.6727.8925 1.2355.7768.5628-.1157.922-.651.8026-1.1957-.417-1.9-.7104-3.84-.8976-5.8637-.0513-.5545-.5574-.964-1.1305-.9142-.573.0497-.996.5396-.9448 1.0942.1944 2.1015.4998 4.121.9348 6.103zM149.995 127.5248c.296.454.9528.61 1.4668.3485.514-.2614.6907-.8413.3947-1.2952-1.0787-1.6535-2.0046-3.3145-2.7896-4.9916-.2266-.484-.8547-.7143-1.403-.5142-.548.2-.809.7546-.5823 1.2387.8208 1.7534 1.788 3.4886 2.9134 5.2138zM154.8088 135.226c1.0587 1.232 2.242 2.4097 3.543 3.531.404.3482 1.0276.3186 1.393-.066.3657-.3843.3346-.978-.0692-1.3262-1.2296-1.0597-2.345-2.17-3.3402-3.328-.195-.227-.3872-.4542-.5764-.6813-.3385-.4063-.9588-.4744-1.3856-.1522-.4267.3223-.4983.913-.1598 1.3192.1954.2346.3938.469.5952.7034zM170.634 146.9026c.4806.242 1.0517.0176 1.2758-.501.224-.5188.0162-1.1354-.4642-1.3773-1.7563-.8842-3.422-1.8432-4.9857-2.8726-.4527-.298-1.0434-.1435-1.3195.3452-.276.4885-.133 1.126.3198 1.424 1.6256 1.0704 3.354 2.0655 5.1738 2.9816z"/><path fill="#EEE" d="M184.7334 151.9698c.5527.1412 1.1072-.2262 1.2385-.8206.1312-.5944-.2104-1.1908-.763-1.332-2.001-.5114-3.9602-1.1002-5.8632-1.763-.5405-.1883-1.1205.1303-1.2955.7115-.175.5813.1212 1.205.6616 1.3934 1.9557.6813 3.9676 1.286 6.0214 1.8108zM197.9337 153.9977c.5532.04 1.0297-.445 1.0643-1.083.0346-.6383-.3857-1.188-.939-1.228-1.973-.1424-3.952-.3682-5.9206-.676-.5492-.086-1.0547.358-1.1292.9917-.0744.6336.3105 1.2168.8597 1.3027 2.0164.3154 4.0433.5467 6.0647.6927zM212.1213 152.6062c.5493-.055.9392-.4576.871-.8994-.0684-.442-.569-.7555-1.1184-.7006-1.9168.1917-3.893.3194-5.9104.382-.553.0173-.9842.392-.9628.8368.0213.445.487.7916 1.0402.7744 2.0737-.0645 4.1064-.1957 6.0803-.3932zM226.3665 149.949c.5293-.22.7755-.8162.5497-1.332-.2257-.5155-.838-.7553-1.3672-.5354-1.7815.74-3.7143 1.3827-5.7772 1.923-.5558.1454-.8852.7023-.7358 1.2436.1494.5414.721.8623 1.2768.7168 2.1547-.5643 4.1797-1.2376 6.0537-2.016zM237.8486 140.4168c.292-.4344.1488-1.006-.3202-1.2766-.469-.2706-1.086-.1378-1.3782.2967-.9575 1.4237-2.225 2.7337-3.7847 3.9202-.427.3248-.4888.9087-.138 1.3042.3505.3955.981.4528 1.408.128 1.723-1.3107 3.1363-2.7714 4.213-4.3726zM245.6725 130.6874c.3987-.3503.439-.9587.09-1.3588-.3492-.4-.9554-.4405-1.3542-.0902-1.5048 1.3222-2.8978 2.7094-4.1698 4.1635-.3497.3995-.3102 1.008.088 1.3587.3983.3508 1.0046.3113 1.3542-.0884 1.2153-1.389 2.5487-2.717 3.9918-3.985zM257.4814 122.8697c.476-.2568.657-.8577.4047-1.342-.2523-.4843-.8428-.6687-1.3188-.4118-1.7682.9542-3.4795 1.973-5.1228 3.0587-.4518.2985-.5803.9133-.287 1.373.2934.46.8975.5906 1.3494.292 1.5938-1.0528 3.2557-2.0423 4.9746-2.97zM270.276 116.9216c.5503-.1682.8513-.724.6723-1.241-.179-.5173-.77-.8003-1.3204-.632-1.9296.5898-3.932 1.2728-5.975 2.054-.536.205-.7936.7797-.5754 1.2835.218.504.8294.746 1.3654.541 1.9947-.7628 3.95-1.4298 5.833-2.0054z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.0605 56s-17.4698 33-12 53c5.4697 20 17 32 38 44S78.5 148 107 159s29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108.000000, 173.000000)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(19.897959, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(12.602041, 6.000000) scale(-1, 1) translate(-12.602041, -6.000000) translate(6.102041, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(0.000000, 10.491228)"><g fill="#FC8A51" transform="translate(29.448980, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><g fill="#FC8A51" transform="translate(5.051020, 21.298246) scale(-1, 1) translate(-5.051020, -21.298246) translate(0.551020, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.1633 12.9123H31.041v3H7.1632z"/></g></g><g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(12.755102, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(7.744898, 4.000000) scale(-1, 1) translate(-7.744898, -4.000000) translate(3.244898, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(0.000000, 6.614035)"><g fill="#EEE" transform="translate(18.877551, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><g fill="#EEE" transform="translate(3.122449, 13.622807) scale(-1, 1) translate(-3.122449, -13.622807) translate(0.122449, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.5918 8.1404h15.306v2H4.592z"/></g></g><g fill="#FFF" transform="translate(0.000000, 103.000000)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g><g transform="translate(39.000000, 142.000000)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.7322 13.475l-1.7665-1.7667c-.5873-.5873-1.5368-.587-2.1226-.0012-.5897.59-.585 1.5362.0013 2.1226l2.826 2.826.0007.0007.0006.0006c.5898.5897 1.534.587 2.118.003l6.3704-6.3703c.577-.577.5826-1.5323-.003-2.118-.59-.59-1.5343-.5873-2.1183-.0033l-5.3065 5.3065z"/></g></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.7045-7.7456 11.0126-20.9255 4.8625-31.5777-7.0208-12.1604-22.4055-16.422-34.363-9.5183-11.9572 6.9036-15.959 22.358-8.9382 34.5183 6.2353 10.8 19.068 15.3695 30.2375 11.4206l10.8992 18.8778c1.3167 2.2807 4.2302 3.063 6.5078 1.748 2.273-1.3122 3.0567-4.2295 1.74-6.51l-10.9458-18.9587zm-8.4343-4.6086c7.8576-4.5366 10.4874-14.6923 5.8738-22.6834-4.6137-7.991-14.7237-10.7915-22.5814-6.255-7.8575 4.5368-10.4873 14.6925-5.8737 22.6836 4.6137 7.991 14.7237 10.7915 22.5814 6.2548z"/></g></svg> diff --git a/app/views/shared/empty_states/icons/_labels.svg b/app/views/shared/empty_states/icons/_labels.svg deleted file mode 100644 index dc041a4a78b..00000000000 --- a/app/views/shared/empty_states/icons/_labels.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zM35 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM374.7 133.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zM318 226.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg> diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg deleted file mode 100644 index 7c672538097..00000000000 --- a/app/views/shared/empty_states/icons/_pipelines_empty.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd"><g transform="translate(0 102)"><g fill="#e5e5e5"><rect width="74" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g fill="#31af64" transform="translate(0 4)"><path fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(73 4)"><path stroke="#e5e5e5" stroke-width="4" d="m64.82 76h33.18c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99v60.02c0 4.413 3.583 7.99 8 7.99h31.935l9.263 9.855c1.725 1.835 4.631 1.833 6.354 0l9.263-9.855"/><rect width="18" height="6" x="11" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="35" y="35" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="29" y="51" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="35" y="19" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="53" y="51" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="11" y="51" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="77" y="19" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="11" y="35" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="53" y="19" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="65" y="19" rx="3"/><rect width="6" height="6" x="71" y="35" rx="3"/></g><rect width="6" height="6" x="59" y="35" fill="#e52c5a" rx="3"/></g><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26" transform="matrix(.70711-.70711.70711.70711 84.34 49.5)"/></g></svg> diff --git a/app/views/shared/empty_states/icons/_pipelines_failed.svg b/app/views/shared/empty_states/icons/_pipelines_failed.svg deleted file mode 100644 index 7dbabf7e4ef..00000000000 --- a/app/views/shared/empty_states/icons/_pipelines_failed.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12-5.464 0-10.652 1.185-15.32 3.311-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088-16.11 3.999-28.06 18.561-28.06 35.912 0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="2" cx="41" cy="41" rx="41" ry="41"/><mask id="1" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="matrix(.86603.5-.5.86603 228.11 137.43)"><path stroke="#b5a7dd" stroke-width="4" d="m.445.161c15.89 10.636 34.998 16.839 55.55 16.839"/><g transform="translate(56 4)"><path fill="#fb722e" d="m16 8c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2m0 10c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2"/><path fill="#fde5d8" fill-rule="nonzero" d="m4 22h6c3.315 0 6-2.685 6-5.997v-6.01c0-3.315-2.684-5.997-6-5.997h-6v18m-4-18.992c0-1.661 1.343-3.01 2.994-3.01h7.01c5.523 0 10 4.47 10 9.997v6.01c0 5.521-4.476 9.997-10 9.997h-7.01c-1.654 0-2.994-1.343-2.994-3.01v-19.984"/></g></g><g fill-rule="nonzero" transform="translate(257)"><path fill="#e5e5e5" d="m3.597 18.747c5.611-9.09 15.519-14.747 26.403-14.747 17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466c4.786-6.111 7.431-13.639 7.431-21.565 0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><g transform="matrix(.96593.25882-.25882.96593 15.98 9.578)"><path fill="#b5a7dd" d="m12.426 11.592l-2.142 1.768-3.664-2.116c-.186-.107-.43-.042-.543.154l-1.229 2.129c-.116.2-.052.438.138.547l3.658 2.112-.455 2.735c-.109.657-.165 1.327-.165 2.01 0 .678.055 1.348.165 2.01l.455 2.735-3.658 2.112c-.186.107-.251.351-.138.547l1.229 2.129c.116.2.353.264.543.154l3.664-2.116 2.142 1.768c1.036.855 2.205 1.533 3.462 2l2.6.972v4.225c0 .215.179.393.405.393h2.458c.231 0 .405-.174.405-.393v-4.225l2.6-.972c1.257-.47 2.426-1.147 3.462-2l2.142-1.768 3.664 2.116c.186.107.43.042.543-.154l1.229-2.129c.116-.2.052-.438-.138-.547l-3.658-2.112.455-2.735c.109-.657.165-1.327.165-2.01 0-.678-.055-1.348-.165-2.01l-.455-2.735 3.658-2.112c.186-.107.251-.351.138-.547l-1.229-2.129c-.116-.2-.353-.264-.543-.154l-3.664 2.116-2.142-1.768c-1.036-.855-2.205-1.533-3.462-2l-2.6-.972v-4.225c0-.215-.179-.393-.405-.393h-2.458c-.231 0-.405.174-.405.393v4.225l-2.6.972c-1.257.47-2.426 1.147-3.462 2m2.062-5.749v-1.45c0-2.426 1.963-4.393 4.405-4.393h2.458c2.433 0 4.405 1.967 4.405 4.393v1.45c1.689.631 3.243 1.538 4.608 2.665l1.259-.727c2.101-1.213 4.786-.497 6.01 1.618l1.229 2.129c1.216 2.107.499 4.798-1.602 6.01l-1.257.726c.144.866.219 1.755.219 2.662 0 .907-.075 1.796-.219 2.662l1.257.726c2.101 1.213 2.823 3.896 1.602 6.01l-1.229 2.129c-1.216 2.107-3.906 2.832-6.01 1.618l-1.259-.727c-1.365 1.127-2.92 2.034-4.608 2.665v1.45c0 2.426-1.963 4.393-4.405 4.393h-2.458c-2.433 0-4.405-1.967-4.405-4.393v-1.45c-1.689-.631-3.243-1.538-4.608-2.665l-1.259.727c-2.101 1.213-4.786.497-6.01-1.618l-1.229-2.129c-1.216-2.107-.499-4.798 1.602-6.01l1.257-.726c-.144-.866-.219-1.755-.219-2.662 0-.907.075-1.796.219-2.662l-1.257-.726c-2.101-1.213-2.823-3.896-1.602-6.01l1.229-2.129c1.216-2.107 3.906-2.832 6.01-1.618l1.259.727c1.365-1.127 2.92-2.034 4.608-2.665"/><path fill="#6b4fbb" d="m20.12 23.366c1.347 0 2.439-1.092 2.439-2.439 0-1.347-1.092-2.439-2.439-2.439-1.347 0-2.439 1.092-2.439 2.439 0 1.347 1.092 2.439 2.439 2.439m0 4c-3.556 0-6.439-2.883-6.439-6.439 0-3.556 2.883-6.439 6.439-6.439 3.556 0 6.439 2.883 6.439 6.439 0 3.556-2.883 6.439-6.439 6.439"/></g></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#1)" stroke-linejoin="round" xlink:href="#0"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="m41 78c20.435 0 37-16.565 37-37 0-20.435-16.565-37-37-37-20.435 0-37 16.565-37 37 0 20.435 16.565 37 37 37m0 4c-22.644 0-41-18.356-41-41 0-22.644 18.356-41 41-41 22.644 0 41 18.356 41 41 0 22.644-18.356 41-41 41"/><g transform="matrix(.96593.25882-.25882.96593 23.581 9.415)"><path fill="#b5a7dd" d="m14.821 13.655l-2.142 1.768-3.933-2.271c-.72-.416-1.634-.171-2.046.543l-1.507 2.61c-.409.708-.161 1.631.553 2.043l3.926 2.267-.455 2.735c-.145.869-.218 1.754-.218 2.65 0 .896.073 1.782.218 2.65l.455 2.735-3.926 2.267c-.72.416-.965 1.329-.553 2.043l1.507 2.61c.409.708 1.332.955 2.046.543l3.933-2.271 2.142 1.768c1.369 1.131 2.916 2.027 4.579 2.648l2.6.972v4.534c0 .831.669 1.5 1.493 1.5h3.01c.817 0 1.493-.676 1.493-1.5v-4.534l2.6-.972c1.663-.621 3.21-1.518 4.579-2.648l2.142-1.768 3.933 2.271c.72.416 1.634.171 2.046-.543l1.507-2.61c.409-.708.161-1.631-.553-2.043l-3.926-2.267.455-2.735c.145-.869.218-1.754.218-2.65 0-.896-.073-1.782-.218-2.65l-.455-2.735 3.926-2.267c.72-.416.965-1.329.553-2.043l-1.507-2.61c-.409-.708-1.332-.955-2.046-.543l-3.933 2.271-2.142-1.768c-1.369-1.131-2.916-2.027-4.579-2.648l-2.6-.972v-4.534c0-.831-.669-1.5-1.493-1.5h-3.01c-.817 0-1.493.676-1.493 1.5v4.534l-2.6.972c-1.663.621-3.21 1.518-4.579 2.648m3.179-6.395v-1.759c0-3.038 2.471-5.5 5.493-5.5h3.01c3.034 0 5.493 2.46 5.493 5.5v1.759c2.098.784 4.03 1.91 5.725 3.311l1.528-.882c2.631-1.519 5.999-.61 7.51 2.01l1.507 2.61c1.517 2.627.616 5.987-2.02 7.507l-1.525.881c.179 1.076.272 2.18.272 3.307 0 1.127-.093 2.231-.272 3.307l1.525.881c2.631 1.519 3.528 4.89 2.02 7.507l-1.507 2.61c-1.517 2.627-4.877 3.527-7.51 2.01l-1.528-.882c-1.696 1.401-3.627 2.527-5.725 3.311v1.759c0 3.038-2.471 5.5-5.493 5.5h-3.01c-3.034 0-5.493-2.46-5.493-5.5v-1.759c-2.098-.784-4.03-1.91-5.725-3.311l-1.528.882c-2.631 1.519-5.999.61-7.51-2.01l-1.507-2.61c-1.517-2.627-.616-5.987 2.02-7.507l1.525-.881c-.179-1.076-.272-2.18-.272-3.307 0-1.127.093-2.231.272-3.307l-1.525-.881c-2.631-1.519-3.528-4.89-2.02-7.507l1.507-2.61c1.517-2.627 4.877-3.527 7.51-2.01l1.528.882c1.696-1.401 3.627-2.527 5.725-3.311"/><path fill="#6b4fbb" d="m25 30c2.209 0 4-1.791 4-4 0-2.209-1.791-4-4-4-2.209 0-4 1.791-4 4 0 2.209 1.791 4 4 4m0 4c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8"/></g></g></g><g transform="translate(140 161)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 8.541v30.01c0 2.202 1.793 3.995 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01c0-2.202-1.793-3.995-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="m445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364-.904.635-1.121 1.883-.486 2.786.635.904 1.883 1.121 2.786.486 15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617.906.632 2.153.41 2.785-.495.632-.906.41-2.153-.495-2.785"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/icons/_todos_all_done.svg b/app/views/shared/empty_states/icons/_todos_all_done.svg deleted file mode 100644 index 94b5c2e0ea0..00000000000 --- a/app/views/shared/empty_states/icons/_todos_all_done.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg> diff --git a/app/views/shared/empty_states/icons/_todos_empty.svg b/app/views/shared/empty_states/icons/_todos_empty.svg deleted file mode 100644 index b1e661268fb..00000000000 --- a/app/views/shared/empty_states/icons/_todos_empty.svg +++ /dev/null @@ -1,110 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - <rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/> - <mask id="l" width="180" height="220" x="0" y="0" fill="#fff"> - <use xlink:href="#a"/> - </mask> - <rect id="b" width="180" height="220" rx="6"/> - <mask id="m" width="180" height="220" x="0" y="0" fill="#fff"> - <use xlink:href="#b"/> - </mask> - <rect id="c" width="28" height="28" rx="4"/> - <mask id="n" width="28" height="28" x="0" y="0" fill="#fff"> - <use xlink:href="#c"/> - </mask> - <rect id="d" width="28" height="28" rx="4"/> - <mask id="o" width="28" height="28" x="0" y="0" fill="#fff"> - <use xlink:href="#d"/> - </mask> - <circle id="e" cx="21.5" cy="21.5" r="21.5"/> - <mask id="p" width="43" height="43" x="0" y="0" fill="#fff"> - <use xlink:href="#e"/> - </mask> - <circle id="f" cx="26.5" cy="26.5" r="26.5"/> - <mask id="q" width="53" height="53" x="0" y="0" fill="#fff"> - <use xlink:href="#f"/> - </mask> - <circle id="g" cx="9.5" cy="4.5" r="4.5"/> - <mask id="r" width="13" height="13" x="-2" y="-2"> - <path fill="#fff" d="M3-2h13v13H3z"/> - <use xlink:href="#g"/> - </mask> - <circle id="h" cx="26.5" cy="26.5" r="26.5"/> - <mask id="s" width="53" height="53" x="0" y="0" fill="#fff"> - <use xlink:href="#h"/> - </mask> - <circle id="i" cx="21.5" cy="21.5" r="21.5"/> - <mask id="t" width="43" height="43" x="0" y="0" fill="#fff"> - <use xlink:href="#i"/> - </mask> - <path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/> - <mask id="u" width="52" height="44" x="0" y="0" fill="#fff"> - <use xlink:href="#j"/> - </mask> - <circle id="k" cx="18.5" cy="18.5" r="18.5"/> - <mask id="v" width="37" height="37" x="0" y="0" fill="#fff"> - <use xlink:href="#k"/> - </mask> - </defs> - <g fill="none" fill-rule="evenodd" transform="translate(-6 -4)"> - <use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/> - <g transform="rotate(5 -707.333 618.042)"> - <use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/> - <g transform="translate(29 24)"> - <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/> - <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/> - <rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/> - <rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/> - </g> - <g transform="translate(29 69)"> - <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/> - <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/> - <rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/> - <rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/> - </g> - <g transform="translate(28 160)"> - <use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/> - <rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/> - <rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/> - </g> - <g transform="translate(28 116)"> - <use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/> - <rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/> - <rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/> - </g> - </g> - <g transform="rotate(-15 601.917 -782.362)"> - <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/> - <text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1"> - <tspan x="12" y="27">@</tspan> - </text> - </g> - <g transform="rotate(15 -686.59 1035.907)"> - <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/> - <path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/> - <g transform="translate(17 14)"> - <use fill="#FC6D26" xlink:href="#g"/> - <use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/> - </g> - </g> - <g transform="rotate(15 -85.125 65.185)"> - <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/> - <path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/> - </g> - <g transform="rotate(-15 716.492 78.873)"> - <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/> - <path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/> - </g> - <g transform="rotate(-15 129.114 -585.74)"> - <use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/> - <circle cx="16" cy="20" r="2" fill="#FC6D26"/> - <circle cx="27" cy="20" r="2" fill="#FC6D26"/> - <circle cx="38" cy="20" r="2" fill="#FC6D26"/> - </g> - <g transform="rotate(-15 1254.8 -458.986)"> - <use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/> - <path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/> - <rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/> - </g> - </g> -</svg> diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg index bd5d04e1cd7..bd5d04e1cd7 100755..100644 --- a/app/views/shared/icons/_icon_status_canceled.svg +++ b/app/views/shared/icons/_icon_status_canceled.svg diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg index 326ad04e017..326ad04e017 100755..100644 --- a/app/views/shared/icons/_icon_status_created.svg +++ b/app/views/shared/icons/_icon_status_created.svg diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg index 64da5aa31fc..64da5aa31fc 100755..100644 --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg index c98839f51a9..c98839f51a9 100755..100644 --- a/app/views/shared/icons/_icon_status_manual.svg +++ b/app/views/shared/icons/_icon_status_manual.svg diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg index 02d5da407e3..02d5da407e3 100755..100644 --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg index 532f4fee33c..532f4fee33c 100755..100644 --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg index a9ba29c922c..a9ba29c922c 100755..100644 --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index eed5006bebe..eed5006bebe 100755..100644 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg index cb785635b7e..cb785635b7e 100755..100644 --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg diff --git a/app/views/shared/icons/_key_2.svg b/app/views/shared/icons/_key_2.svg deleted file mode 100644 index 368b2876c60..00000000000 --- a/app/views/shared/icons/_key_2.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></svg> diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg deleted file mode 100644 index 7267462418e..00000000000 --- a/app/views/shared/icons/_thumbs_up.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg> diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index f16bc8dd430..9ef015047c9 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -3,9 +3,9 @@ - button_method = issuable_close_reopen_button_method(issuable) - if can_update && is_current_user - = link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method, + = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" - = link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method, + = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - elsif can_update && !is_current_user = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml index a38cd319e3c..39a5171c1d6 100644 --- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml +++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml @@ -7,7 +7,7 @@ - button_method = issuable_close_reopen_button_method(issuable) .pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown - = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_url(issuable), + = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable), method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}" = button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color", @@ -16,7 +16,7 @@ %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } } %li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}", - data: { text: "Close #{display_issuable_type}", url: close_issuable_url(issuable), + data: { text: "Close #{display_issuable_type}", url: close_issuable_path(issuable), button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } } %button.btn.btn-transparent = icon('check', class: 'icon') @@ -26,7 +26,7 @@ = display_issuable_type %li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}", - data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_url(issuable), + data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_path(issuable), button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } } %button.btn.btn-transparent = icon('check', class: 'icon') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 9cae3f51825..674f13ddb23 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -26,7 +26,7 @@ = icon('clock-o', 'aria-hidden': 'true') %span.milestone-title - if issuable.milestone - %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_remaining_days(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } } + %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } } = issuable.milestone.title - else None @@ -37,7 +37,7 @@ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' .value.hide-collapsed - if issuable.milestone - = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 } + = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 } - else %span.no-value None diff --git a/app/views/shared/issuable/_user_dropdown_item.html.haml b/app/views/shared/issuable/_user_dropdown_item.html.haml index 48d04678d47..4a3547e9e70 100644 --- a/app/views/shared/issuable/_user_dropdown_item.html.haml +++ b/app/views/shared/issuable/_user_dropdown_item.html.haml @@ -4,7 +4,7 @@ %li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) } %button.btn.btn-link.dropdown-user{ type: :button } .avatar-container.s40 - = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 40, has_tooltip: false).gsub('/images/{{avatar_url}}','{{avatar_url}}').html_safe + = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 40, has_tooltip: false) .dropdown-user-details %span = user.name diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 879e0f99b14..d0ffcc88d43 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -99,8 +99,6 @@ Snippets %div{ class: container_class } - - if @user == current_user && show_callout?('user_callout_dismissed') - = render 'shared/user_callout' .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb new file mode 100644 index 00000000000..ca276d7801c --- /dev/null +++ b/app/workers/project_migrate_hashed_storage_worker.rb @@ -0,0 +1,11 @@ +class ProjectMigrateHashedStorageWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(project_id) + project = Project.find_by(id: project_id) + return if project.nil? || project.pending_delete? + + ::Projects::HashedStorageMigrationService.new(project, logger).execute + end +end diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb new file mode 100644 index 00000000000..b48ead799b9 --- /dev/null +++ b/app/workers/storage_migrator_worker.rb @@ -0,0 +1,30 @@ +class StorageMigratorWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + BATCH_SIZE = 100 + + def perform(start, finish) + projects = build_relation(start, finish) + + projects.with_route.find_each(batch_size: BATCH_SIZE) do |project| + Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." + + begin + project.migrate_to_hashed_storage! + rescue => err + Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") + end + end + end + + def build_relation(start, finish) + relation = Project + table = Project.arel_table + + relation = relation.where(table[:id].gteq(start)) if start + relation = relation.where(table[:id].lteq(finish)) if finish + + relation + end +end diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb deleted file mode 100644 index c9d382cc5d6..00000000000 --- a/app/workers/use_key_worker.rb +++ /dev/null @@ -1,13 +0,0 @@ -class UseKeyWorker - include Sidekiq::Worker - include DedicatedSidekiqQueue - - def perform(key_id) - key = Key.find(key_id) - key.touch(:last_used_at) - rescue ActiveRecord::RecordNotFound - Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job") - - false - end -end diff --git a/changelogs/unreleased/12673-fix_v3_project_hooks_build_events b/changelogs/unreleased/12673-fix_v3_project_hooks_build_events deleted file mode 100644 index 59bc646406f..00000000000 --- a/changelogs/unreleased/12673-fix_v3_project_hooks_build_events +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Fix v3 api project_hooks POST and PUT operations for build_events" -merge_request: 12673 -author: Richard Clamp diff --git a/changelogs/unreleased/12892-reset-css-text-align-to-initial-for-rtl.md b/changelogs/unreleased/12892-reset-css-text-align-to-initial-for-rtl.md deleted file mode 100644 index 87e95240bba..00000000000 --- a/changelogs/unreleased/12892-reset-css-text-align-to-initial-for-rtl.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "reset text-align to initial to let elements with dir="auto" align texts to right in RTL languages ( default is left )" -merge_request: 12892 -author: goshhob diff --git a/changelogs/unreleased/12968-generalize-profile-updates.yml b/changelogs/unreleased/12968-generalize-profile-updates.yml deleted file mode 100644 index d09793512c1..00000000000 --- a/changelogs/unreleased/12968-generalize-profile-updates.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Generalize profile updates from providers -merge_request: 12968 -author: Alexandros Keramidas diff --git a/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml deleted file mode 100644 index 90b169390d2..00000000000 --- a/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: disabling notifications globally now properly turns off group/project added - emails -merge_request: 13325 -author: @jneen -type: fixed diff --git a/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml b/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml new file mode 100644 index 00000000000..5f98d0cc766 --- /dev/null +++ b/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml @@ -0,0 +1,5 @@ +--- +title: Confirmation email shows link as text instead of human readable text +merge_request: 14243 +author: bitsapien +type: changed diff --git a/changelogs/unreleased/13711-allow-same-period-housekeeping.yml b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml index 6749e22cf6a..607a8683aff 100644 --- a/changelogs/unreleased/13711-allow-same-period-housekeeping.yml +++ b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml @@ -2,5 +2,5 @@ title: Allow to use same periods for different housekeeping tasks (effectively skipping the lesser task) merge_request: 13711 -author: @cernvcs +author: cernvcs type: added diff --git a/changelogs/unreleased/14395-upgrade-gitlab-markup.yml b/changelogs/unreleased/14395-upgrade-gitlab-markup.yml new file mode 100644 index 00000000000..d1f90fe5eb1 --- /dev/null +++ b/changelogs/unreleased/14395-upgrade-gitlab-markup.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade gitlab-markup gem +merge_request: 14395 +author: Markus Koller +type: other diff --git a/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml b/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml deleted file mode 100644 index 8ec78bbd41f..00000000000 --- a/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add settings for minimum SSH key strength and allowed key type -merge_request: 13712 -author: Cory Hinshaw -type: added diff --git a/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml b/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml deleted file mode 100644 index 6d5baa8c10f..00000000000 --- a/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Hide admin link from default search results for non-admins -merge_request: 14015 -author: -type: fixed diff --git a/changelogs/unreleased/20049-projects-api-forks.yml b/changelogs/unreleased/20049-projects-api-forks.yml new file mode 100644 index 00000000000..c6470620f57 --- /dev/null +++ b/changelogs/unreleased/20049-projects-api-forks.yml @@ -0,0 +1,5 @@ +--- +title: Add an API endpoint to determine the forks of a project +merge_request: +author: +type: added diff --git a/changelogs/unreleased/21949-add-type-to-changelog.yml b/changelogs/unreleased/21949-add-type-to-changelog.yml deleted file mode 100644 index a20f6b7ad4e..00000000000 --- a/changelogs/unreleased/21949-add-type-to-changelog.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added type to CHANGELOG entries -merge_request: -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email deleted file mode 100644 index f4011b756a5..00000000000 --- a/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Handle unsubscribe from email notifications via replying to reply+%{key}+unsubscribe@ address -merge_request: 6597 -author: diff --git a/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml deleted file mode 100644 index abb9e33d626..00000000000 --- a/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removes Sortable default scope. -merge_request: 13558 -author: -type: fixed diff --git a/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml b/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml deleted file mode 100644 index fa1ca3d25b2..00000000000 --- a/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add CI/CD job predefined variables with user name and login -merge_request: 13824 -author: -type: added diff --git a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys deleted file mode 100644 index 0e8f7093b34..00000000000 --- a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor Timelogs structure to use foreign keys. -merge_request: 8769 -author: diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml deleted file mode 100644 index ed38fd37103..00000000000 --- a/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Decrease ABC threshold to 55.25 -merge_request: 13904 -author: Maxim Rydkin -type: other diff --git a/changelogs/unreleased/28283-uuid-storage.yml b/changelogs/unreleased/28283-uuid-storage.yml deleted file mode 100644 index 283e06d4b7f..00000000000 --- a/changelogs/unreleased/28283-uuid-storage.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Hashed Storage support for Repositories (EXPERIMENTAL) -merge_request: 13246 -author: diff --git a/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml b/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml deleted file mode 100644 index 129cf505a3f..00000000000 --- a/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add time stats to Issue and Merge Request API -merge_request: 13335 -author: @travismiller diff --git a/changelogs/unreleased/28938-password-change-workflow-for-admins.yml b/changelogs/unreleased/28938-password-change-workflow-for-admins.yml deleted file mode 100644 index 0781e1a2fce..00000000000 --- a/changelogs/unreleased/28938-password-change-workflow-for-admins.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Changes the password change workflow for admins. -merge_request: 13901 -author: -type: fixed diff --git a/changelogs/unreleased/29811-fix-line-number-alignment.yml b/changelogs/unreleased/29811-fix-line-number-alignment.yml deleted file mode 100644 index 94b3328a7f2..00000000000 --- a/changelogs/unreleased/29811-fix-line-number-alignment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix the alignment of line numbers to lines of code in code viewer -merge_request: 13403 -author: Trevor Flynn
\ No newline at end of file diff --git a/changelogs/unreleased/30162-retire-koding-integration.yml b/changelogs/unreleased/30162-retire-koding-integration.yml deleted file mode 100644 index 63c2b9eb161..00000000000 --- a/changelogs/unreleased/30162-retire-koding-integration.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Deprecation of Koding integration, removal of setting in Admin Panel -merge_request: 13992 -author: @mydigitalself diff --git a/changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml b/changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml deleted file mode 100644 index 4d21717e161..00000000000 --- a/changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Ensure correct visibility level options shown on all Project, Group, and Snippets - forms -merge_request: 13442 -author: -type: fixed diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml deleted file mode 100644 index 4a8d8097169..00000000000 --- a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Decrease Cyclomatic Complexity threshold to 14 -merge_request: 13972 -author: Maxim Rydkin -type: other diff --git a/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml b/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml deleted file mode 100644 index 06e8180db64..00000000000 --- a/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix group and project search for anonymous users -merge_request: 13745 -author: -type: fixed diff --git a/changelogs/unreleased/31470-fix-api-files-raw.yml b/changelogs/unreleased/31470-fix-api-files-raw.yml deleted file mode 100644 index 271a945a998..00000000000 --- a/changelogs/unreleased/31470-fix-api-files-raw.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix the /projects/:id/repository/files/:file_path/raw endpoint to handle dots in the file_path -merge_request: 13512 -author: mahcsig -type: fixed diff --git a/changelogs/unreleased/32340-correct-jobs-api-documentation b/changelogs/unreleased/32340-correct-jobs-api-documentation deleted file mode 100644 index 4ada62356eb..00000000000 --- a/changelogs/unreleased/32340-correct-jobs-api-documentation +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Correction to documention for manual steps on the Jobs API" -merge_request: 11411 -author: Zac Sturgess
\ No newline at end of file diff --git a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml deleted file mode 100644 index fde70c47ca6..00000000000 --- a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Redesign project feature permissions settings -merge_request: 14062 -author: -type: changed diff --git a/changelogs/unreleased/33287-fix-mr-widget-errors-with-external-services.yml b/changelogs/unreleased/33287-fix-mr-widget-errors-with-external-services.yml deleted file mode 100644 index f0c76060781..00000000000 --- a/changelogs/unreleased/33287-fix-mr-widget-errors-with-external-services.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix errors thrown in merge request widget with external CI service/integration -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml b/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml new file mode 100644 index 00000000000..d3aac241b75 --- /dev/null +++ b/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml @@ -0,0 +1,5 @@ +--- +title: Adds gitlab features and components to usage ping data. +merge_request: 14305 +author: +type: other diff --git a/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml deleted file mode 100644 index 278ef2a8acb..00000000000 --- a/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added tests for commits API unauthenticated user and public/private project -merge_request: 13287 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/34261-move-move-to-sidebar.yml b/changelogs/unreleased/34261-move-move-to-sidebar.yml deleted file mode 100644 index 59fa1d4c221..00000000000 --- a/changelogs/unreleased/34261-move-move-to-sidebar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move "Move issue" controls to right-sidebar -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/34371-cycle-analitcs-global.yml b/changelogs/unreleased/34371-cycle-analitcs-global.yml new file mode 100644 index 00000000000..5e9f0a85e9a --- /dev/null +++ b/changelogs/unreleased/34371-cycle-analitcs-global.yml @@ -0,0 +1,5 @@ +--- +title: Removes cycle analytics service and store from global namespace +merge_request: +author: +type: other diff --git a/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml b/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml deleted file mode 100644 index 7de30d82601..00000000000 --- a/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Improves performance of vue code by using vue files and moving svg out of data - function in pipeline schedule callout -merge_request: -author: -type: other diff --git a/changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml b/changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml deleted file mode 100644 index d33b55ef681..00000000000 --- a/changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move ConvDev Index location to after Cohorts. -merge_request: !13398 -author: diff --git a/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml b/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml deleted file mode 100644 index a61d703bacd..00000000000 --- a/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improves markdown rendering performance for commit lists. -merge_request: -author: -type: other diff --git a/changelogs/unreleased/34643-fix-project-path-slugify.yml b/changelogs/unreleased/34643-fix-project-path-slugify.yml deleted file mode 100644 index f7018a1aca5..00000000000 --- a/changelogs/unreleased/34643-fix-project-path-slugify.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix CI_PROJECT_PATH_SLUG slugify -merge_request: 13350 -author: Ivan Chernov diff --git a/changelogs/unreleased/34945-readme-div-id.yml b/changelogs/unreleased/34945-readme-div-id.yml deleted file mode 100644 index c7d26b746b4..00000000000 --- a/changelogs/unreleased/34945-readme-div-id.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add div id to the readme in the project overview -merge_request: 13735 -author: Riccardo Padovani @rpadovani -type: added diff --git a/changelogs/unreleased/34990-top-buttons-misaligned.yml b/changelogs/unreleased/34990-top-buttons-misaligned.yml deleted file mode 100644 index db60f83ed71..00000000000 --- a/changelogs/unreleased/34990-top-buttons-misaligned.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixes margins on the top buttons of the pipeline table -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/35010-projects-nav-dropdown.yml b/changelogs/unreleased/35010-projects-nav-dropdown.yml deleted file mode 100644 index c5bed723f55..00000000000 --- a/changelogs/unreleased/35010-projects-nav-dropdown.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add dropdown to Projects nav item -merge_request: 13866 -author: -type: added diff --git a/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml b/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml deleted file mode 100644 index 6cd7f4e9cc6..00000000000 --- a/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove project select dropdown from breadcrumb -merge_request: 14010 -author: -type: changed diff --git a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml deleted file mode 100644 index 74aa337a18c..00000000000 --- a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add option in preferences to change navigation theme color -merge_request: -author: -type: added diff --git a/changelogs/unreleased/35048-empty-badges.yml b/changelogs/unreleased/35048-empty-badges.yml deleted file mode 100644 index 816fe82887c..00000000000 --- a/changelogs/unreleased/35048-empty-badges.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevents rendering empty badges when request fails -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/35161_first_time_contributor_badge.yml b/changelogs/unreleased/35161_first_time_contributor_badge.yml deleted file mode 100644 index f3ab2d9db31..00000000000 --- a/changelogs/unreleased/35161_first_time_contributor_badge.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "First-time contributor badge" -merge_request: 13143 -author: Micaël Bergeron <micaelbergeron@gmail.com> diff --git a/changelogs/unreleased/3523-i18n-autodevops.yml b/changelogs/unreleased/3523-i18n-autodevops.yml new file mode 100644 index 00000000000..10cb22b42a0 --- /dev/null +++ b/changelogs/unreleased/3523-i18n-autodevops.yml @@ -0,0 +1,5 @@ +--- +title: Improves i18n for Auto Devops callout +merge_request: +author: +type: other diff --git a/changelogs/unreleased/35343-inherit-milestones-and-labels.yml b/changelogs/unreleased/35343-inherit-milestones-and-labels.yml deleted file mode 100644 index ce737a67356..00000000000 --- a/changelogs/unreleased/35343-inherit-milestones-and-labels.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: inherits milestone and labels when a merge request is created from issue -merge_request: 13461 -author: haseebeqx -type: added diff --git a/changelogs/unreleased/35441-fix-division-by-zero.yml b/changelogs/unreleased/35441-fix-division-by-zero.yml deleted file mode 100644 index 335b2d40494..00000000000 --- a/changelogs/unreleased/35441-fix-division-by-zero.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix division by zero error in blame age mapping -merge_request: 13803 -author: Jeff Stubler -type: fixed diff --git a/changelogs/unreleased/35686-unescape-wiki-title.yml b/changelogs/unreleased/35686-unescape-wiki-title.yml deleted file mode 100644 index 4b2b7078163..00000000000 --- a/changelogs/unreleased/35686-unescape-wiki-title.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Unescape HTML characters in Wiki title -merge_request: 13942 -author: Jacopo Beschi @jacopo-beschi -type: fixed diff --git a/changelogs/unreleased/35721-auth-style-confirmation.yml b/changelogs/unreleased/35721-auth-style-confirmation.yml deleted file mode 100644 index 9963f76e845..00000000000 --- a/changelogs/unreleased/35721-auth-style-confirmation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: restyling of OAuth authorization confirmation -merge_request: -author: Jacopo Beschi @jacopo-beschi -type: changed diff --git a/changelogs/unreleased/35793_fix_predicate_names.yml b/changelogs/unreleased/35793_fix_predicate_names.yml deleted file mode 100644 index d4da177dc2e..00000000000 --- a/changelogs/unreleased/35793_fix_predicate_names.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove `is_` prefix from predicate method names -merge_request: 13810 -author: Maxim Rydkin -type: other diff --git a/changelogs/unreleased/35811-copy-link-note.yml b/changelogs/unreleased/35811-copy-link-note.yml deleted file mode 100644 index 9fa74884c8a..00000000000 --- a/changelogs/unreleased/35811-copy-link-note.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add support for copying permalink to notes via more actions dropdown -merge_request: 13299 -author: -type: added diff --git a/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml b/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml deleted file mode 100644 index eac8dbe23c2..00000000000 --- a/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improves subgroup creation permissions -merge_request: 13418 -author: -type: bugifx diff --git a/changelogs/unreleased/35942-api-binary-encoding.yaml b/changelogs/unreleased/35942-api-binary-encoding.yaml deleted file mode 100644 index 4f7960d860e..00000000000 --- a/changelogs/unreleased/35942-api-binary-encoding.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: "Fix API to serve binary diffs that are treated as text." -merge_request: 14038 diff --git a/changelogs/unreleased/35978-milestone-title.yml b/changelogs/unreleased/35978-milestone-title.yml deleted file mode 100644 index 1a4b71328c1..00000000000 --- a/changelogs/unreleased/35978-milestone-title.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Truncate milestone title if sidebar is collapsed -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/35994-archived-projects-only.yml b/changelogs/unreleased/35994-archived-projects-only.yml deleted file mode 100644 index ce565b177d0..00000000000 --- a/changelogs/unreleased/35994-archived-projects-only.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add an option to list only archived projects -merge_request: 13492 -author: Mehdi Lahmam (@mehlah) -type: added diff --git a/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml b/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml deleted file mode 100644 index 04791e09b84..00000000000 --- a/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add checks for branch existence before changing HEAD -merge_request: 13359 -author: Vitaliy @blackst0ne Klachkov diff --git a/changelogs/unreleased/36041-notification-title.yml b/changelogs/unreleased/36041-notification-title.yml deleted file mode 100644 index 7c5e0a0cd0d..00000000000 --- a/changelogs/unreleased/36041-notification-title.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't escape html entities in InlineDiffMarkdownMarker -merge_request: -author: diff --git a/changelogs/unreleased/36087-users-cannot-delete-their-account.yml b/changelogs/unreleased/36087-users-cannot-delete-their-account.yml deleted file mode 100644 index 9ba75d8b1d0..00000000000 --- a/changelogs/unreleased/36087-users-cannot-delete-their-account.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: allow all users to delete their account -merge_request: 13636 -author: Jacopo Beschi @jacopo-beschi -type: changed diff --git a/changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml b/changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml deleted file mode 100644 index 1b664efb8c2..00000000000 --- a/changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Present enqueued merge jobs as Merging as well -merge_request: -author: diff --git a/changelogs/unreleased/36119-issuable-workers.yml b/changelogs/unreleased/36119-issuable-workers.yml deleted file mode 100644 index beb01ae5b1a..00000000000 --- a/changelogs/unreleased/36119-issuable-workers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Simplify checking if objects exist code in new issaubles workers -merge_request: -author: diff --git a/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml b/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml deleted file mode 100644 index b51b5e58b39..00000000000 --- a/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Include the `is_admin` field in the `GET /users/:id` API when current user - is an admin -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml b/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml deleted file mode 100644 index 356857d6e8a..00000000000 --- a/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Merge request reference in merge commit changed to full reference -merge_request: 13518 -author: haseebeqx -type: fixed diff --git a/changelogs/unreleased/36385-pipeline-graph-dropdown.yml b/changelogs/unreleased/36385-pipeline-graph-dropdown.yml deleted file mode 100644 index 1a43c66debd..00000000000 --- a/changelogs/unreleased/36385-pipeline-graph-dropdown.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevents jobs dropdown from closing in pipeline graph -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml b/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml new file mode 100644 index 00000000000..f5ccb163d98 --- /dev/null +++ b/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml @@ -0,0 +1,5 @@ +--- +title: Allow the git circuit breaker to correctly handle missing repository storages +merge_request: 14417 +author: +type: fixed diff --git a/changelogs/unreleased/36611-error-in-getcomposer-link.yml b/changelogs/unreleased/36611-error-in-getcomposer-link.yml deleted file mode 100644 index 1ff6ec01684..00000000000 --- a/changelogs/unreleased/36611-error-in-getcomposer-link.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix external link to Composer website -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml b/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml new file mode 100644 index 00000000000..a2e1d07158b --- /dev/null +++ b/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml @@ -0,0 +1,6 @@ +--- +title: Reschedule merge request diff background migrations to catch failures from + 9.5 run +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml deleted file mode 100644 index 76c9c905590..00000000000 --- a/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Normalize styles for empty state combo button -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml b/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml deleted file mode 100644 index be08da0433a..00000000000 --- a/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Never wait for sidekiq jobs when creating projects -merge_request: 13775 -author: -type: other diff --git a/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml b/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml deleted file mode 100644 index a37de4325bb..00000000000 --- a/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove unwanted refs after importing a project -merge_request: 13766 -author: -type: other diff --git a/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml b/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml deleted file mode 100644 index 54c7a8c8788..00000000000 --- a/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix new navigation wrapping and causing height to grow -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml deleted file mode 100644 index e48a5704fdd..00000000000 --- a/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update gpg documentation with gpg2 -merge_request: 13851 -author: M M Arif -type: other diff --git a/changelogs/unreleased/36860-migrate-issues-author.yml b/changelogs/unreleased/36860-migrate-issues-author.yml deleted file mode 100644 index 3e9fcc55836..00000000000 --- a/changelogs/unreleased/36860-migrate-issues-author.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Migrate issues authored by deleted user to the Ghost user -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml b/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml deleted file mode 100644 index a06c84c30e6..00000000000 --- a/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Disable GitLab Project Import Button if source disabled -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36884-gitaly-admin-version.yml b/changelogs/unreleased/36884-gitaly-admin-version.yml new file mode 100644 index 00000000000..0b3b9a205b5 --- /dev/null +++ b/changelogs/unreleased/36884-gitaly-admin-version.yml @@ -0,0 +1,5 @@ +--- +title: Add Gitaly version to Admin Dashboard +merge_request: 14313 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/36917-branch-tooltip.yml b/changelogs/unreleased/36917-branch-tooltip.yml deleted file mode 100644 index 2d37de50cec..00000000000 --- a/changelogs/unreleased/36917-branch-tooltip.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds tooltip to the branch name and improves performance -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36937-fix-invite-by-email-text.yml b/changelogs/unreleased/36937-fix-invite-by-email-text.yml deleted file mode 100644 index 06c6105fab6..00000000000 --- a/changelogs/unreleased/36937-fix-invite-by-email-text.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix invite by email address duplication -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/36939-fix-find-blobs-by-path.yml b/changelogs/unreleased/36939-fix-find-blobs-by-path.yml deleted file mode 100644 index b48b10049ed..00000000000 --- a/changelogs/unreleased/36939-fix-find-blobs-by-path.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix searching for files by path -merge_request: 13798 -author: -type: fixed diff --git a/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml index 680ef0cef92..9ac4a0ae7f3 100644 --- a/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml +++ b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml @@ -1,5 +1,5 @@ --- title: Add GitLab-Pages version to Admin Dashboard merge_request: 14040 -author: @travismiller +author: travismiller type: added diff --git a/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml b/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml deleted file mode 100644 index 83f6b2d21e1..00000000000 --- a/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add repository toggle for automatically resolving outdated diff discussions -merge_request: 14053 -author: AshleyDumaine -type: added diff --git a/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml b/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml deleted file mode 100644 index fcaa6ec13f8..00000000000 --- a/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove focus styles from dropdown empty links -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37104-fix-graph-date-format.yml b/changelogs/unreleased/37104-fix-graph-date-format.yml deleted file mode 100644 index f7d39fe8283..00000000000 --- a/changelogs/unreleased/37104-fix-graph-date-format.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix incorrect date/time formatting on prometheus graphs -merge_request: 13865 -author: -type: fixed diff --git a/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml b/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml deleted file mode 100644 index 34161e63c81..00000000000 --- a/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Better align fallback image emojis -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37158-autodevops-banner.yml b/changelogs/unreleased/37158-autodevops-banner.yml deleted file mode 100644 index f0ffb670102..00000000000 --- a/changelogs/unreleased/37158-autodevops-banner.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Created callout for auto devops -merge_request: -author: -type: added diff --git a/changelogs/unreleased/37179-dashboard-project-dropdown.yml b/changelogs/unreleased/37179-dashboard-project-dropdown.yml deleted file mode 100644 index 3ef080b8eae..00000000000 --- a/changelogs/unreleased/37179-dashboard-project-dropdown.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removes disabled state from dashboard project button -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml b/changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml deleted file mode 100644 index ef83dc1d10a..00000000000 --- a/changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'API: Respect default group visibility when creating a group' -merge_request: 13903 -author: Robert Schilling -type: fixed diff --git a/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml b/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml deleted file mode 100644 index 593e74593c4..00000000000 --- a/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Deprecate custom SSH client configuration for the git user -merge_request: 13930 -author: -type: deprecated diff --git a/changelogs/unreleased/37220-es-modules.yml b/changelogs/unreleased/37220-es-modules.yml new file mode 100644 index 00000000000..de81fa9e74d --- /dev/null +++ b/changelogs/unreleased/37220-es-modules.yml @@ -0,0 +1,5 @@ +--- +title: Exports common_utils utility functions as modules +merge_request: +author: +type: other diff --git a/changelogs/unreleased/37259-some-mr-ready-mobile-fixes.yml b/changelogs/unreleased/37259-some-mr-ready-mobile-fixes.yml deleted file mode 100644 index a00a41f567f..00000000000 --- a/changelogs/unreleased/37259-some-mr-ready-mobile-fixes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix MR ready to merge buttons/controls at mobile breakpoint -merge_request: 14242 -author: -type: fixed diff --git a/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml deleted file mode 100644 index d6d21ac4c51..00000000000 --- a/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix a wrong `X-Gitlab-Event` header when testing webhooks -merge_request: 14108 -author: -type: fixed diff --git a/changelogs/unreleased/37331-button-MR-widget.yml b/changelogs/unreleased/37331-button-MR-widget.yml deleted file mode 100644 index 59bc1bd201e..00000000000 --- a/changelogs/unreleased/37331-button-MR-widget.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix buttons with different height in merge request widget -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37335-counter-active-state.yml b/changelogs/unreleased/37335-counter-active-state.yml new file mode 100644 index 00000000000..a9632201a89 --- /dev/null +++ b/changelogs/unreleased/37335-counter-active-state.yml @@ -0,0 +1,5 @@ +--- +title: Add active states to nav bar counters +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/37368-blob-viewer-on-mobile.yml b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml deleted file mode 100644 index 6a955f5715f..00000000000 --- a/changelogs/unreleased/37368-blob-viewer-on-mobile.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make blob viewer for rich contents wider for mobile -merge_request: 14011 -author: Takuya Noguchi -type: fixed diff --git a/changelogs/unreleased/37406-success-status-icon.yml b/changelogs/unreleased/37406-success-status-icon.yml deleted file mode 100644 index faac947f188..00000000000 --- a/changelogs/unreleased/37406-success-status-icon.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix broken svg in jobs dropdown for success status -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37465-fix-line-resolve-all-green-checkmark-icon.yml b/changelogs/unreleased/37465-fix-line-resolve-all-green-checkmark-icon.yml deleted file mode 100644 index 24b1d201409..00000000000 --- a/changelogs/unreleased/37465-fix-line-resolve-all-green-checkmark-icon.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Update x/x discussions resolved checkmark icon to be green when all discussions - resolved -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37467-helper-method-from-users-endpoint-overrides-api-helper-method.yml b/changelogs/unreleased/37467-helper-method-from-users-endpoint-overrides-api-helper-method.yml new file mode 100644 index 00000000000..1984ec6e81c --- /dev/null +++ b/changelogs/unreleased/37467-helper-method-from-users-endpoint-overrides-api-helper-method.yml @@ -0,0 +1,5 @@ +--- +title: find_user Users helper method no longer overrides find_user API helper method. +merge_request: 14418 +author: +type: fixed diff --git a/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml b/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml deleted file mode 100644 index 2a8e8a33225..00000000000 --- a/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add description template examples to documentation -merge_request: -author: -type: other diff --git a/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml b/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml deleted file mode 100644 index 8c328eb0950..00000000000 --- a/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix the diff file header from being html escaped for renamed files. -merge_request: 14121 -author: -type: fixed diff --git a/changelogs/unreleased/37590-pipelines-mr.yml b/changelogs/unreleased/37590-pipelines-mr.yml deleted file mode 100644 index ee609888155..00000000000 --- a/changelogs/unreleased/37590-pipelines-mr.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix mini graph pipeline breakin in merge request view -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml b/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml deleted file mode 100644 index 5735d59a2bd..00000000000 --- a/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Image attachments are properly displayed in notification emails again -merge_request: 14161 -author: -type: fixed diff --git a/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml b/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml deleted file mode 100644 index 7357c61b5f2..00000000000 --- a/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve Image onion skin + swipe does not work anymore -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml b/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml deleted file mode 100644 index 6894e650c11..00000000000 --- a/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow using newlines in pipeline email service recipients -merge_request: 14250 -author: -type: fixed diff --git a/changelogs/unreleased/37999-fix-circuit-breaker.yml b/changelogs/unreleased/37999-fix-circuit-breaker.yml new file mode 100644 index 00000000000..a75315c4988 --- /dev/null +++ b/changelogs/unreleased/37999-fix-circuit-breaker.yml @@ -0,0 +1,5 @@ +--- +title: Fix the filesystem shard health check to check all configured shards +merge_request: 14341 +author: +type: fixed diff --git a/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml b/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml new file mode 100644 index 00000000000..df562077fb3 --- /dev/null +++ b/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml @@ -0,0 +1,5 @@ +--- +title: Fix `rake gitlab:incoming_email:check` and make it report the actual error +merge_request: 14423 +author: +type: fixed diff --git a/changelogs/unreleased/38234-reserve-refs-replace.yml b/changelogs/unreleased/38234-reserve-refs-replace.yml new file mode 100644 index 00000000000..3a5ffbf9db0 --- /dev/null +++ b/changelogs/unreleased/38234-reserve-refs-replace.yml @@ -0,0 +1,5 @@ +--- +title: Also reserve refs/replace after importing a project +merge_request: 14436 +author: +type: fixed diff --git a/changelogs/unreleased/38319-nomethoderror-undefined-method-sha-for-nil-nilclass.yml b/changelogs/unreleased/38319-nomethoderror-undefined-method-sha-for-nil-nilclass.yml new file mode 100644 index 00000000000..f3c39827590 --- /dev/null +++ b/changelogs/unreleased/38319-nomethoderror-undefined-method-sha-for-nil-nilclass.yml @@ -0,0 +1,5 @@ +--- +title: Fix 500 error on merged merge requests when GitLab is restored from a backup +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/38432-fix-notes-type-for-import.yml b/changelogs/unreleased/38432-fix-notes-type-for-import.yml new file mode 100644 index 00000000000..db8371f4420 --- /dev/null +++ b/changelogs/unreleased/38432-fix-notes-type-for-import.yml @@ -0,0 +1,6 @@ +--- +title: Fix notes type created from import. This should fix some missing notes issues + from imported projects +merge_request: 14524 +author: +type: fixed diff --git a/changelogs/unreleased/38476-improve-merge-jid-cleanup-on-merge-process.yml b/changelogs/unreleased/38476-improve-merge-jid-cleanup-on-merge-process.yml new file mode 100644 index 00000000000..43dec51029b --- /dev/null +++ b/changelogs/unreleased/38476-improve-merge-jid-cleanup-on-merge-process.yml @@ -0,0 +1,5 @@ +--- +title: Adjust MRs being stuck on "process of being merged" for more than 2 hours +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/38528-build-url.yml b/changelogs/unreleased/38528-build-url.yml new file mode 100644 index 00000000000..357b9aacea8 --- /dev/null +++ b/changelogs/unreleased/38528-build-url.yml @@ -0,0 +1,5 @@ +--- +title: Fixes data parameter not being sent in ajax request for jobs log +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/38582-popover-badge.yml b/changelogs/unreleased/38582-popover-badge.yml new file mode 100644 index 00000000000..ccec679a13f --- /dev/null +++ b/changelogs/unreleased/38582-popover-badge.yml @@ -0,0 +1,5 @@ +--- +title: Improves UX of autodevops popover to match gpg one +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml b/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml new file mode 100644 index 00000000000..9a600282716 --- /dev/null +++ b/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml @@ -0,0 +1,5 @@ +--- +title: Add index for merge_requests.merge_commit_sha +merge_request: +author: +type: other diff --git a/changelogs/unreleased/add-filter-by-my-reaction.yml b/changelogs/unreleased/add-filter-by-my-reaction.yml deleted file mode 100644 index dc1601cf3ee..00000000000 --- a/changelogs/unreleased/add-filter-by-my-reaction.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add my reaction filter to search bar -merge_request: 12962 -author: Hiroyuki Sato diff --git a/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml b/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml deleted file mode 100644 index 4c81d21a94b..00000000000 --- a/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added mock deployment and monitoring service with environments fixtures -merge_request: -author: diff --git a/changelogs/unreleased/add-view-replaced-file-link.yml b/changelogs/unreleased/add-view-replaced-file-link.yml new file mode 100644 index 00000000000..b5a85f2e0f2 --- /dev/null +++ b/changelogs/unreleased/add-view-replaced-file-link.yml @@ -0,0 +1,5 @@ +--- +title: Add view replaced file link for image diffs +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/add_message_to_the_404_page.yml b/changelogs/unreleased/add_message_to_the_404_page.yml deleted file mode 100644 index f567796fe9f..00000000000 --- a/changelogs/unreleased/add_message_to_the_404_page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Changed message and title on the 404 page -merge_request: -author: Branka Martinovic -type: added diff --git a/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml b/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml deleted file mode 100644 index 3e4105a4232..00000000000 --- a/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add quick submission on user settings page -merge_request: 14007 -author: Vitaliy @blackst0ne Klachkov -type: added diff --git a/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml b/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml new file mode 100644 index 00000000000..0470c6519f4 --- /dev/null +++ b/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml @@ -0,0 +1,5 @@ +--- +title: Add tooltip for milestone due date to issue and merge request lists +merge_request: 14318 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/changelogs/unreleased/additional-time-series-charts.yml b/changelogs/unreleased/additional-time-series-charts.yml deleted file mode 100644 index 80c1af54881..00000000000 --- a/changelogs/unreleased/additional-time-series-charts.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added support the multiple time series for prometheus monitoring -merge_request: !36893 -author: -type: changed diff --git a/changelogs/unreleased/api-delete-respect-headers.yml b/changelogs/unreleased/api-delete-respect-headers.yml deleted file mode 100644 index cfc8fbfdf91..00000000000 --- a/changelogs/unreleased/api-delete-respect-headers.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'API: Respect the "If-Unmodified-Since" header when delting a resource' -merge_request: 9621 -author: Robert Schilling -type: added diff --git a/changelogs/unreleased/api-gpg-key-management.yml b/changelogs/unreleased/api-gpg-key-management.yml deleted file mode 100644 index 0be35a5823b..00000000000 --- a/changelogs/unreleased/api-gpg-key-management.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'API: Add GPG key management' -merge_request: 13828 -author: Robert Schilling -type: added diff --git a/changelogs/unreleased/api_branches_head.yml b/changelogs/unreleased/api_branches_head.yml deleted file mode 100644 index 68d8d3d5168..00000000000 --- a/changelogs/unreleased/api_branches_head.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add branch existence check to the APIv4 branches via HEAD request -merge_request: 13979 -author: Vitaliy @blackst0ne Klachkov -type: added diff --git a/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml b/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml deleted file mode 100644 index fd0b7c4f43c..00000000000 --- a/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fire hooks asynchronously when creating a new job to improve performance -merge_request: 13734 -author: -type: changed diff --git a/changelogs/unreleased/breadcrumb-item-links.yml b/changelogs/unreleased/breadcrumb-item-links.yml new file mode 100644 index 00000000000..9b66456efca --- /dev/null +++ b/changelogs/unreleased/breadcrumb-item-links.yml @@ -0,0 +1,5 @@ +--- +title: Fixed issue/merge request breadcrumb titles not having links +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/breadcrumbs-line-height-padding.yml b/changelogs/unreleased/breadcrumbs-line-height-padding.yml new file mode 100644 index 00000000000..3ac56c8b593 --- /dev/null +++ b/changelogs/unreleased/breadcrumbs-line-height-padding.yml @@ -0,0 +1,5 @@ +--- +title: breadcrumbs receives padding when double lined +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml b/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml deleted file mode 100644 index 7571999fa75..00000000000 --- a/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bumps omniauth-ldap gem version to 2.0.4 -merge_request: 13465 -author: diff --git a/changelogs/unreleased/bvl-fix-close-issuable-link.yml b/changelogs/unreleased/bvl-fix-close-issuable-link.yml new file mode 100644 index 00000000000..140a9d35cc1 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-close-issuable-link.yml @@ -0,0 +1,5 @@ +--- +title: Fix CSRF validation issue when closing/opening merge requests from the UI +merge_request: 14555 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-improve-bare-project-import.yml b/changelogs/unreleased/bvl-improve-bare-project-import.yml deleted file mode 100644 index 74c1da4ea40..00000000000 --- a/changelogs/unreleased/bvl-improve-bare-project-import.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 'Improve bare project import: Allow subgroups, take default visibility level - into account' -merge_request: 13670 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-validate-po-files.yml b/changelogs/unreleased/bvl-validate-po-files.yml deleted file mode 100644 index f840b2c3973..00000000000 --- a/changelogs/unreleased/bvl-validate-po-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Validate PO-files in static analysis -merge_request: 13000 -author: diff --git a/changelogs/unreleased/cache-issue-and-mr-counts.yml b/changelogs/unreleased/cache-issue-and-mr-counts.yml deleted file mode 100644 index fe3fe3be976..00000000000 --- a/changelogs/unreleased/cache-issue-and-mr-counts.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Cache the number of open issues and merge requests -merge_request: -author: -type: other diff --git a/changelogs/unreleased/changes-tab-jumping.yml b/changelogs/unreleased/changes-tab-jumping.yml deleted file mode 100644 index 5740dfade9f..00000000000 --- a/changelogs/unreleased/changes-tab-jumping.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed merge request changes bar jumping -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/check-trigger-permissions.yml b/changelogs/unreleased/check-trigger-permissions.yml deleted file mode 100644 index e0809cea9bf..00000000000 --- a/changelogs/unreleased/check-trigger-permissions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve migrations using triggers -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/ci-environment-status-performance.yml b/changelogs/unreleased/ci-environment-status-performance.yml deleted file mode 100644 index 8812733b5a7..00000000000 --- a/changelogs/unreleased/ci-environment-status-performance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Constrain environment deployments to project IDs -merge_request: -author: -type: other diff --git a/changelogs/unreleased/collapsable-pipeline-settings.yml b/changelogs/unreleased/collapsable-pipeline-settings.yml deleted file mode 100644 index d41959f8ab0..00000000000 --- a/changelogs/unreleased/collapsable-pipeline-settings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add collapsable sections for Pipeline Settings -merge_request: -author: -type: added diff --git a/changelogs/unreleased/commit-side-by-side-comment.yml b/changelogs/unreleased/commit-side-by-side-comment.yml new file mode 100644 index 00000000000..f9bea285a77 --- /dev/null +++ b/changelogs/unreleased/commit-side-by-side-comment.yml @@ -0,0 +1,5 @@ +--- +title: Fixed commenting on side-by-side commit diff +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/conv-dev-index-regression.yml b/changelogs/unreleased/conv-dev-index-regression.yml deleted file mode 100644 index 799eafa4265..00000000000 --- a/changelogs/unreleased/conv-dev-index-regression.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix ConvDev Index nav item and Monitoring submenu regression -merge_request: !14124 -author: -type: fixed diff --git a/changelogs/unreleased/dashboards-projects-controller.yml b/changelogs/unreleased/dashboards-projects-controller.yml deleted file mode 100644 index 8b350f70a80..00000000000 --- a/changelogs/unreleased/dashboards-projects-controller.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Eager load namespace owners for project dashboards -merge_request: -author: -type: other diff --git a/changelogs/unreleased/detect-orphaned-repositories.yml b/changelogs/unreleased/detect-orphaned-repositories.yml deleted file mode 100644 index 101c1897826..00000000000 --- a/changelogs/unreleased/detect-orphaned-repositories.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Scripts to detect orphaned repositories -merge_request: 14204 -author: -type: added diff --git a/changelogs/unreleased/disable-project-export.yml b/changelogs/unreleased/disable-project-export.yml deleted file mode 100644 index d7ca9f46193..00000000000 --- a/changelogs/unreleased/disable-project-export.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add option to disable project export on instance -merge_request: 13211 -author: Robin Bobbitt diff --git a/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml b/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml deleted file mode 100644 index f4a956e6724..00000000000 --- a/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Disallow NULL values for environments.project_id" -merge_request: -author: -type: other diff --git a/changelogs/unreleased/dm-api-unauthorized.yml b/changelogs/unreleased/dm-api-unauthorized.yml new file mode 100644 index 00000000000..26b45bd4c40 --- /dev/null +++ b/changelogs/unreleased/dm-api-unauthorized.yml @@ -0,0 +1,5 @@ +--- +title: Make sure API responds with 401 when invalid authentication info is provided +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml b/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml new file mode 100644 index 00000000000..057407b78d9 --- /dev/null +++ b/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml @@ -0,0 +1,6 @@ +--- +title: Fix bug that caused merge requests with diff notes imported from Bitbucket + to raise errors +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/dm-closing-issue-urls.yml b/changelogs/unreleased/dm-closing-issue-urls.yml new file mode 100644 index 00000000000..059e406b63d --- /dev/null +++ b/changelogs/unreleased/dm-closing-issue-urls.yml @@ -0,0 +1,5 @@ +--- +title: Correctly detect multiple issue URLs after 'Closes...' in MR descriptions +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/dm-simple-project-avatar-url.yml b/changelogs/unreleased/dm-simple-project-avatar-url.yml new file mode 100644 index 00000000000..e517345f5d2 --- /dev/null +++ b/changelogs/unreleased/dm-simple-project-avatar-url.yml @@ -0,0 +1,5 @@ +--- +title: Expose avatar_url when requesting list of projects from API with simple=true +merge_request: +author: +type: added diff --git a/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml b/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml new file mode 100644 index 00000000000..3cdcff8caaf --- /dev/null +++ b/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml @@ -0,0 +1,5 @@ +--- +title: Clarify artifact download via the API only accepts branch or tag name for ref +merge_request: +author: +type: other diff --git a/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml b/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml new file mode 100644 index 00000000000..eea679d0814 --- /dev/null +++ b/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml @@ -0,0 +1,5 @@ +--- +title: Change recommended MySQL version to 5.6 +merge_request: +author: +type: other diff --git a/changelogs/unreleased/docs-confidential-issue.yml b/changelogs/unreleased/docs-confidential-issue.yml deleted file mode 100644 index 841970ef4cf..00000000000 --- a/changelogs/unreleased/docs-confidential-issue.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update documentation for confidential issue -merge_request: 14117 -author: -type: other diff --git a/changelogs/unreleased/docs-document-version-for-group-milestones-api.yml b/changelogs/unreleased/docs-document-version-for-group-milestones-api.yml deleted file mode 100644 index d75c46313f4..00000000000 --- a/changelogs/unreleased/docs-document-version-for-group-milestones-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Document version Group Milestones API introduced -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/docs-fix-15669-issue-move-api.yml b/changelogs/unreleased/docs-fix-15669-issue-move-api.yml deleted file mode 100644 index db68428fda3..00000000000 --- a/changelogs/unreleased/docs-fix-15669-issue-move-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add to_project_id parameter to Move Issue via API example -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml b/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml deleted file mode 100644 index d8a5073f110..00000000000 --- a/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update 'Using Docker images' documentation -merge_request: 13848 -author: -type: other diff --git a/changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml b/changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml deleted file mode 100644 index a7db18dbd60..00000000000 --- a/changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed add diff note button not showing after deleting a comment -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/ee-add-project-repository-storages-index.yml b/changelogs/unreleased/ee-add-project-repository-storages-index.yml new file mode 100644 index 00000000000..8a144783cec --- /dev/null +++ b/changelogs/unreleased/ee-add-project-repository-storages-index.yml @@ -0,0 +1,5 @@ +--- +title: Index projects on repository storage +merge_request: 14414 +author: +type: other diff --git a/changelogs/unreleased/events-migration-cleanup.yml b/changelogs/unreleased/events-migration-cleanup.yml deleted file mode 100644 index 1e3e843f252..00000000000 --- a/changelogs/unreleased/events-migration-cleanup.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Finish migration to the new events setup -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml b/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml new file mode 100644 index 00000000000..d16e052cd92 --- /dev/null +++ b/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml @@ -0,0 +1,5 @@ +--- +title: Expose last pipeline details in API response when getting a single commit +merge_request: 13521 +author: Mehdi Lahmam (@mehlah) +type: added diff --git a/changelogs/unreleased/feature-custom-attributes.yml b/changelogs/unreleased/feature-custom-attributes.yml new file mode 100644 index 00000000000..98736bc8d72 --- /dev/null +++ b/changelogs/unreleased/feature-custom-attributes.yml @@ -0,0 +1,4 @@ +--- +title: Support custom attributes on users +merge_request: 13038 +author: Markus Koller diff --git a/changelogs/unreleased/feature-dependency-status-badge.yml b/changelogs/unreleased/feature-dependency-status-badge.yml deleted file mode 100644 index 1becff3585a..00000000000 --- a/changelogs/unreleased/feature-dependency-status-badge.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add badge for dependency status -merge_request: 13588 -author: Markus Koller -type: other diff --git a/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml b/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml deleted file mode 100644 index 920679ca166..00000000000 --- a/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make it possible to download a single job artifact file using the API -merge_request: 14027 -author: -type: added diff --git a/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml b/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml deleted file mode 100644 index 00c38a0c671..00000000000 --- a/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add CI/CD active kubernetes job policy -merge_request: 13849 -author: -type: added diff --git a/changelogs/unreleased/feature-gpg-verification-status.yml b/changelogs/unreleased/feature-gpg-verification-status.yml deleted file mode 100644 index 7518fafcdb8..00000000000 --- a/changelogs/unreleased/feature-gpg-verification-status.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 'Update the GPG verification semantics: A GPG signature must additionally match - the committer in order to be verified' -merge_request: 13771 -author: Alexis Reigel -type: changed diff --git a/changelogs/unreleased/feature-plantuml-restructured-text.yml b/changelogs/unreleased/feature-plantuml-restructured-text.yml deleted file mode 100644 index b885029f589..00000000000 --- a/changelogs/unreleased/feature-plantuml-restructured-text.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add documentation for PlantUML in reStructuredText -merge_request: 13900 -author: Markus Koller -type: other diff --git a/changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml b/changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml deleted file mode 100644 index b57b9a3dfbe..00000000000 --- a/changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Protected runners -merge_request: 13194 -author: -type: added diff --git a/changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml b/changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml deleted file mode 100644 index 969a5aeaed3..00000000000 --- a/changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Extend API: Pipeline Schedule Variable' -merge_request: 13653 -author: -type: added diff --git a/changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml b/changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml deleted file mode 100644 index 006b0b45844..00000000000 --- a/changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Implement `failure_reason` on `ci_builds` -merge_request: 13937 -author: -type: added diff --git a/changelogs/unreleased/fix-btn-alignment.yml b/changelogs/unreleased/fix-btn-alignment.yml deleted file mode 100644 index e5dce3d3a0e..00000000000 --- a/changelogs/unreleased/fix-btn-alignment.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix inconsistent spacing for edit buttons on issues and merge request page -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-edit-merge-request-button-case.yml b/changelogs/unreleased/fix-edit-merge-request-button-case.yml deleted file mode 100644 index 8550f3e3c1b..00000000000 --- a/changelogs/unreleased/fix-edit-merge-request-button-case.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix edit merge request and issues button inconsistent letter casing -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-gpg-tmp-dir-removal-race-condition.yml b/changelogs/unreleased/fix-gpg-tmp-dir-removal-race-condition.yml deleted file mode 100644 index e75f188913f..00000000000 --- a/changelogs/unreleased/fix-gpg-tmp-dir-removal-race-condition.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixes the 500 errors caused by a race condition in GPG's tmp directory handling -merge_request: 14194 -author: Alexis Reigel -type: fixed diff --git a/changelogs/unreleased/fix-image-diff-swipe-handle.yml b/changelogs/unreleased/fix-image-diff-swipe-handle.yml new file mode 100644 index 00000000000..a4e0c2e8465 --- /dev/null +++ b/changelogs/unreleased/fix-image-diff-swipe-handle.yml @@ -0,0 +1,5 @@ +--- +title: Fix image diff swipe handle offset to correctly align with the frame +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-import-export-performance.yml b/changelogs/unreleased/fix-import-export-performance.yml deleted file mode 100644 index 1f59c4eb179..00000000000 --- a/changelogs/unreleased/fix-import-export-performance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve Import/Export memory usage -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-import-fork-mr.yml b/changelogs/unreleased/fix-import-fork-mr.yml deleted file mode 100644 index 4e9cf7faae8..00000000000 --- a/changelogs/unreleased/fix-import-fork-mr.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix Import/Export issue to do with fork merge requests -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-mr-sidebar-counter-after-merge.yml b/changelogs/unreleased/fix-mr-sidebar-counter-after-merge.yml new file mode 100644 index 00000000000..22a3efb8b1e --- /dev/null +++ b/changelogs/unreleased/fix-mr-sidebar-counter-after-merge.yml @@ -0,0 +1,5 @@ +--- +title: Fix merge request counter updates after merge +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-multi-line-hook-output.yml b/changelogs/unreleased/fix-multi-line-hook-output.yml new file mode 100644 index 00000000000..f625ec2ee6c --- /dev/null +++ b/changelogs/unreleased/fix-multi-line-hook-output.yml @@ -0,0 +1,5 @@ +--- +title: Display full pre-receive and post-receive hook output in GitLab UI +merge_request: 14222 +author: Robin Bobbitt +type: fixed diff --git a/changelogs/unreleased/fix-npm-security-updates.yml b/changelogs/unreleased/fix-npm-security-updates.yml deleted file mode 100644 index faa0c3149b8..00000000000 --- a/changelogs/unreleased/fix-npm-security-updates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade brace-expansion NPM package due to security issue -merge_request: 13665 -author: Markus Koller -type: security diff --git a/changelogs/unreleased/fix-sm-37559-pipeline-triggered-through-api-not-showing-trigger-variables.yml b/changelogs/unreleased/fix-sm-37559-pipeline-triggered-through-api-not-showing-trigger-variables.yml deleted file mode 100644 index 8aae0f6f5b6..00000000000 --- a/changelogs/unreleased/fix-sm-37559-pipeline-triggered-through-api-not-showing-trigger-variables.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix Pipeline Triggers to show triggered label and predefined variables (e.g. - CI_PIPELINE_TRIGGERED) -merge_request: 14244 -author: -type: fixed diff --git a/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml b/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml deleted file mode 100644 index ae4df3ee31a..00000000000 --- a/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix stray OR in New Project page -merge_request: 14096 -author: Robin Bobbitt -type: fixed diff --git a/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml b/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml deleted file mode 100644 index fa50e36e28a..00000000000 --- a/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix typo in the API Deploy Keys documentation page -merge_request: 14014 -author: Vitaliy @blackst0ne Klachkov -type: fixed diff --git a/changelogs/unreleased/fix_wiki_toc_indent.yml b/changelogs/unreleased/fix_wiki_toc_indent.yml deleted file mode 100644 index 60da2e455f2..00000000000 --- a/changelogs/unreleased/fix_wiki_toc_indent.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Wiki table of contents are now properly nested to reflect header level -merge_request: 13650 -author: Akihiro Nakashima -type: fixed diff --git a/changelogs/unreleased/font-weight-adjusted.yml b/changelogs/unreleased/font-weight-adjusted.yml deleted file mode 100644 index 827f3485099..00000000000 --- a/changelogs/unreleased/font-weight-adjusted.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Changed all font-weight values to 400 and 600 and introduced 2 variables to - manage them -merge_request: !12896 -author: diff --git a/changelogs/unreleased/force-two-up-view.yml b/changelogs/unreleased/force-two-up-view.yml new file mode 100644 index 00000000000..1074eb384bb --- /dev/null +++ b/changelogs/unreleased/force-two-up-view.yml @@ -0,0 +1,5 @@ +--- +title: Force two up view to load by default for image diffs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fork-btn-enabled-user-groups.yml b/changelogs/unreleased/fork-btn-enabled-user-groups.yml new file mode 100644 index 00000000000..3bd7581a961 --- /dev/null +++ b/changelogs/unreleased/fork-btn-enabled-user-groups.yml @@ -0,0 +1,5 @@ +--- +title: Fixed fork button being disabled for users who can fork to a group +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fuzzy-issue-search.yml b/changelogs/unreleased/fuzzy-issue-search.yml deleted file mode 100644 index 8195e97ed59..00000000000 --- a/changelogs/unreleased/fuzzy-issue-search.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support a multi-word fuzzy seach issues/merge requests on search bar -merge_request: 13780 -author: Hiroyuki Sato -type: changed diff --git a/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml deleted file mode 100644 index 90c25d782c8..00000000000 --- a/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Gitaly feature toggles are on by default in development -merge_request: 13802 -author: -type: other diff --git a/changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml b/changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml deleted file mode 100644 index edf11484d1f..00000000000 --- a/changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make Gitaly PostUploadPack mandatory -merge_request: 13953 -author: -type: changed diff --git a/changelogs/unreleased/gitaly_ref_exists.yml b/changelogs/unreleased/gitaly_ref_exists.yml deleted file mode 100644 index f62b646e406..00000000000 --- a/changelogs/unreleased/gitaly_ref_exists.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Implement the Gitaly RefService::RefExists endpoint -merge_request: 13528 -author: Andrew Newdigate diff --git a/changelogs/unreleased/group-mr-search-bar.yml b/changelogs/unreleased/group-mr-search-bar.yml deleted file mode 100644 index 0b554a5d7c9..00000000000 --- a/changelogs/unreleased/group-mr-search-bar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add filtered search to group merge requests dashboard -merge_request: 13688 -author: Hiroyuki Sato -type: changed diff --git a/changelogs/unreleased/hash-mr-scroll-load.yml b/changelogs/unreleased/hash-mr-scroll-load.yml new file mode 100644 index 00000000000..7e3965add03 --- /dev/null +++ b/changelogs/unreleased/hash-mr-scroll-load.yml @@ -0,0 +1,5 @@ +--- +title: Fixed notes not being scrolled to in merge requests +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/hashed-storage-migration-path.yml b/changelogs/unreleased/hashed-storage-migration-path.yml new file mode 100644 index 00000000000..5890eb09c38 --- /dev/null +++ b/changelogs/unreleased/hashed-storage-migration-path.yml @@ -0,0 +1,5 @@ +--- +title: Script to migrate project's repositories to new Hashed Storage +merge_request: 14067 +author: +type: added diff --git a/changelogs/unreleased/improve-autocomplete-user-performance.yml b/changelogs/unreleased/improve-autocomplete-user-performance.yml deleted file mode 100644 index 5a7153771ff..00000000000 --- a/changelogs/unreleased/improve-autocomplete-user-performance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve performance for AutocompleteController#users.json -merge_request: 13754 -author: Hiroyuki Sato -type: changed diff --git a/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml b/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml deleted file mode 100644 index ec0d1b245e4..00000000000 --- a/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: '"Share with group lock" now applies to subgroups, but owner can override setting - on subgroups' -merge_request: 13944 -author: -type: changed diff --git a/changelogs/unreleased/improve_sorting_list.yml b/changelogs/unreleased/improve_sorting_list.yml new file mode 100644 index 00000000000..a3730e23ed1 --- /dev/null +++ b/changelogs/unreleased/improve_sorting_list.yml @@ -0,0 +1,5 @@ +--- +title: Improve list of sorting options +merge_request: 14320 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/changelogs/unreleased/issue-api-my-reaction.yml b/changelogs/unreleased/issue-api-my-reaction.yml deleted file mode 100644 index 1c12478fbc0..00000000000 --- a/changelogs/unreleased/issue-api-my-reaction.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add my_reaction_emoji param to /issues and /merge_requests API -merge_request: 14016 -author: Hiroyuki Sato -type: added diff --git a/changelogs/unreleased/issue-boards-breadcrumbs-container.yml b/changelogs/unreleased/issue-boards-breadcrumbs-container.yml deleted file mode 100644 index 5e042de7000..00000000000 --- a/changelogs/unreleased/issue-boards-breadcrumbs-container.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix breadcrumbs container in issue boards -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/issue_37640.yml b/changelogs/unreleased/issue_37640.yml deleted file mode 100644 index d806ed64bed..00000000000 --- a/changelogs/unreleased/issue_37640.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix project feature being deleted when updating project with invalid visibility - level -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/lint-changelog-yaml.yml b/changelogs/unreleased/lint-changelog-yaml.yml new file mode 100644 index 00000000000..dcc8bf54827 --- /dev/null +++ b/changelogs/unreleased/lint-changelog-yaml.yml @@ -0,0 +1,5 @@ +--- +title: Detect when changelog entries are invalid +merge_request: +author: +type: other diff --git a/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml deleted file mode 100644 index 5a7cd42b888..00000000000 --- a/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Memoize the latest builds of a pipeline on a project's homepage" -merge_request: -author: -type: other diff --git a/changelogs/unreleased/mk-clarify-moving-namespaces.yml b/changelogs/unreleased/mk-clarify-moving-namespaces.yml new file mode 100644 index 00000000000..8d89c296f61 --- /dev/null +++ b/changelogs/unreleased/mk-clarify-moving-namespaces.yml @@ -0,0 +1,5 @@ +--- +title: Expand docs for changing username or group path +merge_request: 13914 +author: +type: other diff --git a/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml b/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml deleted file mode 100644 index 865b57fb284..00000000000 --- a/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Default LDAP config "verify_certificates" to true for security -merge_request: 13915 -author: -type: changed diff --git a/changelogs/unreleased/move-action.yml b/changelogs/unreleased/move-action.yml deleted file mode 100644 index 65eceae3ef9..00000000000 --- a/changelogs/unreleased/move-action.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow users to move issues to other projects using a / command -merge_request: 13436 -author: Manolis Mavrofidis diff --git a/changelogs/unreleased/mr-index-page-performance.yml b/changelogs/unreleased/mr-index-page-performance.yml deleted file mode 100644 index df5f44c04fa..00000000000 --- a/changelogs/unreleased/mr-index-page-performance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Re-use issue/MR counts for the pagination system -merge_request: -author: -type: other diff --git a/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml b/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml new file mode 100644 index 00000000000..39b636bdfda --- /dev/null +++ b/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml @@ -0,0 +1,5 @@ +--- +title: Fixed breadcrumbs container expanding in side-by-side diff view +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/perf-slow-issuable.yml b/changelogs/unreleased/perf-slow-issuable.yml deleted file mode 100644 index 29d15be1401..00000000000 --- a/changelogs/unreleased/perf-slow-issuable.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix repository equality check and avoid fetching ref if the commit is already - available. This affects merge request creation performance -merge_request: 13685 -author: -type: other diff --git a/changelogs/unreleased/project-page-clearer.yml b/changelogs/unreleased/project-page-clearer.yml new file mode 100644 index 00000000000..7db01373360 --- /dev/null +++ b/changelogs/unreleased/project-page-clearer.yml @@ -0,0 +1,5 @@ +--- +title: Added tabs to dashboard/projects to easily switch to personal projects +merge_request: +author: +type: added diff --git a/changelogs/unreleased/projects-controller-show.yml b/changelogs/unreleased/projects-controller-show.yml deleted file mode 100644 index 25f4a72710b..00000000000 --- a/changelogs/unreleased/projects-controller-show.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Memoize pipelines for project download buttons -merge_request: -author: -type: other diff --git a/changelogs/unreleased/remove-temporary-ci-index.yml b/changelogs/unreleased/remove-temporary-ci-index.yml new file mode 100644 index 00000000000..a319f7fff7f --- /dev/null +++ b/changelogs/unreleased/remove-temporary-ci-index.yml @@ -0,0 +1,5 @@ +--- +title: Remove an index on ci_builds meant to be only temporary +merge_request: +author: +type: other diff --git a/changelogs/unreleased/remove-use-key-worker.yml b/changelogs/unreleased/remove-use-key-worker.yml new file mode 100644 index 00000000000..a39bcae66bc --- /dev/null +++ b/changelogs/unreleased/remove-use-key-worker.yml @@ -0,0 +1,5 @@ +--- +title: Stop using Sidekiq for updating Key#last_used_at +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/replace_project_service-feature.yml b/changelogs/unreleased/replace_project_service-feature.yml new file mode 100644 index 00000000000..11814732a9b --- /dev/null +++ b/changelogs/unreleased/replace_project_service-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/service.feature' spinach test with an rspec analog +merge_request: 14432 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_project_shortcuts-feature.yml b/changelogs/unreleased/replace_project_shortcuts-feature.yml new file mode 100644 index 00000000000..89e47a7a983 --- /dev/null +++ b/changelogs/unreleased/replace_project_shortcuts-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/shortcuts.feature' spinach test with an rspec analog +merge_request: 14431 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_spinach_search_code-feature.yml b/changelogs/unreleased/replace_spinach_search_code-feature.yml deleted file mode 100644 index 28d2108c871..00000000000 --- a/changelogs/unreleased/replace_spinach_search_code-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace 'source/search_code.feature' spinach test with an rspec analog -merge_request: 13697 -author: blackst0ne -type: other diff --git a/changelogs/unreleased/replace_spinach_star-feature.yml b/changelogs/unreleased/replace_spinach_star-feature.yml deleted file mode 100644 index 6a058691fe5..00000000000 --- a/changelogs/unreleased/replace_spinach_star-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace 'project/star.feature' spinach test with an rspec analog -merge_request: 13855 -author: Vitaliy @blackst0ne Klachkov -type: other diff --git a/changelogs/unreleased/replace_spinach_user_lookup-feature.yml b/changelogs/unreleased/replace_spinach_user_lookup-feature.yml deleted file mode 100644 index 36248c54d99..00000000000 --- a/changelogs/unreleased/replace_spinach_user_lookup-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace 'project/user_lookup.feature' spinach test with an rspec analog -merge_request: 13863 -author: Vitaliy @blackst0ne Klachkov -type: other diff --git a/changelogs/unreleased/repository-name-emojis b/changelogs/unreleased/repository-name-emojis deleted file mode 100644 index fe52df8eedc..00000000000 --- a/changelogs/unreleased/repository-name-emojis +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added ability to put emojis into repository name -merge_request: 7420 -author: Vincent Composieux diff --git a/changelogs/unreleased/rotated_profile_image.yml b/changelogs/unreleased/rotated_profile_image.yml new file mode 100644 index 00000000000..1e221e47379 --- /dev/null +++ b/changelogs/unreleased/rotated_profile_image.yml @@ -0,0 +1,5 @@ +--- +title: Fix profile image orientation based on EXIF data gvieira37 +merge_request: 14461 +author: gvieira37 +type: fixed diff --git a/changelogs/unreleased/rouge-2-2-0.yml b/changelogs/unreleased/rouge-2-2-0.yml deleted file mode 100644 index 0b53cd14628..00000000000 --- a/changelogs/unreleased/rouge-2-2-0.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bump rouge to v2.2.0 -merge_request: 13633 -author: -type: other diff --git a/changelogs/unreleased/rouge-2-2-1.yml b/changelogs/unreleased/rouge-2-2-1.yml deleted file mode 100644 index 2d8879e5574..00000000000 --- a/changelogs/unreleased/rouge-2-2-1.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bump rouge to v2.2.1 -merge_request: 13887 -author: -type: other diff --git a/changelogs/unreleased/seven-days-cycle-analytics.yml b/changelogs/unreleased/seven-days-cycle-analytics.yml deleted file mode 100644 index ff660bdd603..00000000000 --- a/changelogs/unreleased/seven-days-cycle-analytics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add a `Last 7 days` option for Cycle Analytics view -merge_request: 13443 -author: Mehdi Lahmam (@mehlah) -type: added diff --git a/changelogs/unreleased/sh-add-grape-logging.yml b/changelogs/unreleased/sh-add-grape-logging.yml deleted file mode 100644 index eaf6cb045d5..00000000000 --- a/changelogs/unreleased/sh-add-grape-logging.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add JSON logger in `log/api_json.log` for Grape API endpoints -merge_request: -author: -type: added diff --git a/changelogs/unreleased/sh-bump-jira-gem.yml b/changelogs/unreleased/sh-bump-jira-gem.yml deleted file mode 100644 index d76b688caac..00000000000 --- a/changelogs/unreleased/sh-bump-jira-gem.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bump jira-ruby gem to 1.4.1 to fix issues with HTTP proxies -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sh-optimize-discussion-json.yml b/changelogs/unreleased/sh-optimize-discussion-json.yml deleted file mode 100644 index 4be1bc89a91..00000000000 --- a/changelogs/unreleased/sh-optimize-discussion-json.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Eliminate N+1 queries in loading discussions.json endpoint -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sidebar-cache-updates.yml b/changelogs/unreleased/sidebar-cache-updates.yml deleted file mode 100644 index aebe53ba5b2..00000000000 --- a/changelogs/unreleased/sidebar-cache-updates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only update the sidebar count caches when needed -merge_request: -author: -type: other diff --git a/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml b/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml deleted file mode 100644 index 602ca358b8b..00000000000 --- a/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add 'from commit' information to cherry-picked commits -merge_request: 13475 -author: Saverio Miroddi -type: added diff --git a/changelogs/unreleased/support-additional-colors.yml b/changelogs/unreleased/support-additional-colors.yml deleted file mode 100644 index 5178e159dcf..00000000000 --- a/changelogs/unreleased/support-additional-colors.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added support for specific labels and colors -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml b/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml deleted file mode 100644 index 218336df5d2..00000000000 --- a/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Migration to remove pending delete projects with non-existing namespace -merge_request: 13598 -author: -type: other diff --git a/changelogs/unreleased/url-sanitizer-fixes.yml b/changelogs/unreleased/url-sanitizer-fixes.yml deleted file mode 100644 index 769036c829c..00000000000 --- a/changelogs/unreleased/url-sanitizer-fixes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix problems sanitizing URLs with empty passwords -merge_request: 14083 -author: -type: fixed diff --git a/changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml b/changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml deleted file mode 100644 index 0c3acce1455..00000000000 --- a/changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use full path of project's avatar in webhooks -merge_request: 13649 -author: Vitaliy @blackst0ne Klachkov -type: changed diff --git a/changelogs/unreleased/user-recent-push.yml b/changelogs/unreleased/user-recent-push.yml deleted file mode 100644 index defd5cdfd8e..00000000000 --- a/changelogs/unreleased/user-recent-push.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Rework how recent push events are retrieved -merge_request: -author: -type: other diff --git a/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml b/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml new file mode 100644 index 00000000000..faa5d3303c6 --- /dev/null +++ b/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml @@ -0,0 +1,5 @@ +--- +title: Removed two legacy config options +merge_request: +author: Daniel Voogsgerd +type: deprecated diff --git a/changelogs/unreleased/wiki_api.yml b/changelogs/unreleased/wiki_api.yml deleted file mode 100644 index 9d60356aedc..00000000000 --- a/changelogs/unreleased/wiki_api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add API support for wiki pages -merge_request: 13372 -author: Vitaliy @blackst0ne Klachkov -type: added diff --git a/changelogs/unreleased/winh-dropdown-changelog-docs.yml b/changelogs/unreleased/winh-dropdown-changelog-docs.yml deleted file mode 100644 index 2f42b4dd9f9..00000000000 --- a/changelogs/unreleased/winh-dropdown-changelog-docs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Restyle dropdown menus to make them look consistent -merge_request: -author: -type: other diff --git a/changelogs/unreleased/zj-add-pipeline-source-variable.yml b/changelogs/unreleased/zj-add-pipeline-source-variable.yml deleted file mode 100644 index 5d98cd8086a..00000000000 --- a/changelogs/unreleased/zj-add-pipeline-source-variable.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add CI_PIPELINE_SOURCE variable on CI Jobs -merge_request: -author: -type: added diff --git a/changelogs/unreleased/zj-auto-devops-banner.yml b/changelogs/unreleased/zj-auto-devops-banner.yml deleted file mode 100644 index a2abed0b2ec..00000000000 --- a/changelogs/unreleased/zj-auto-devops-banner.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Do not show the Auto DevOps banner when the project has a .gitlab-ci.yml on - master -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/zj-auto-devops-table.yml b/changelogs/unreleased/zj-auto-devops-table.yml deleted file mode 100644 index f1a004ebd19..00000000000 --- a/changelogs/unreleased/zj-auto-devops-table.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow users and administrator to configure Auto-DevOps -merge_request: 13923 -author: -type: added diff --git a/changelogs/unreleased/zj-disable-pages-in-subgroups.yml b/changelogs/unreleased/zj-disable-pages-in-subgroups.yml deleted file mode 100644 index 22c36214e1f..00000000000 --- a/changelogs/unreleased/zj-disable-pages-in-subgroups.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove pages settings when not available -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/zj-feature-flipper-disable-banner.yml b/changelogs/unreleased/zj-feature-flipper-disable-banner.yml deleted file mode 100644 index fd5dd1bbe37..00000000000 --- a/changelogs/unreleased/zj-feature-flipper-disable-banner.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow all AutoDevOps banners to be turned off -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/zj-remove-ci-api-v1.yml b/changelogs/unreleased/zj-remove-ci-api-v1.yml deleted file mode 100644 index 8f2dc321b36..00000000000 --- a/changelogs/unreleased/zj-remove-ci-api-v1.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove CI API v1 -merge_request: -author: -type: removed diff --git a/changelogs/unreleased/zj-repo-gitaly.yml b/changelogs/unreleased/zj-repo-gitaly.yml new file mode 100644 index 00000000000..634f6ba1b8b --- /dev/null +++ b/changelogs/unreleased/zj-repo-gitaly.yml @@ -0,0 +1,5 @@ +--- +title: Gitaly RepositoryExists remains opt-in for all method calls +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml b/changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml deleted file mode 100644 index 474392a8cdd..00000000000 --- a/changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reword job to pipeline to reflect what the graphs are really about -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/zj-sort-templates.yml b/changelogs/unreleased/zj-sort-templates.yml deleted file mode 100644 index 443c4355890..00000000000 --- a/changelogs/unreleased/zj-sort-templates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Sort templates in the dropdown -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/zj-update-rails-template.yml b/changelogs/unreleased/zj-update-rails-template.yml deleted file mode 100644 index 5464f0e3d42..00000000000 --- a/changelogs/unreleased/zj-update-rails-template.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update Rails project template to use Postgresql by default -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/zj-upgrade-grape.yml b/changelogs/unreleased/zj-upgrade-grape.yml deleted file mode 100644 index daa6a234c07..00000000000 --- a/changelogs/unreleased/zj-upgrade-grape.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade grape to 1.0 -merge_request: -author: -type: other diff --git a/changelogs/unreleased/zj-usage-data-auto-devops.yml b/changelogs/unreleased/zj-usage-data-auto-devops.yml deleted file mode 100644 index 9b5ec894042..00000000000 --- a/changelogs/unreleased/zj-usage-data-auto-devops.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add usage data for Auto DevOps -merge_request: -author: -type: other diff --git a/config/application.rb b/config/application.rb index da9bb25c8b9..30117b6a98e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,8 +105,6 @@ 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/dependency_decisions.yml b/config/dependency_decisions.yml index 6c5c8cad270..db31b01a7d2 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -452,3 +452,15 @@ :why: https://github.com/jaredhanson/utils-merge/blob/v1.0.0/LICENSE :versions: [] :when: 2017-09-16 05:18:26.193764000 Z +- - :approve + - svg4everybody + - :who: Tim Zallmann + :why: CC0 1.0 - https://github.com/jonathantneal/svg4everybody/blob/master/LICENSE.md + :versions: [] + :when: 2017-09-13 17:31:16.425819400 Z +- - :approve + - gitlab-svgs + - :who: Tim Zallmann + :why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs + :versions: [] + :when: 2017-09-19 14:36:32.795496000 Z diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cd44f888d3f..9b496822e93 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -577,12 +577,6 @@ production: &base # Use the default values unless you really know what you are doing git: bin_path: /usr/bin/git - # The next value is the maximum memory size grit can use - # Given in number of bytes per git object (e.g. a commit) - # This value can be increased if you have very large commits - max_size: 20971520 # 20.megabytes - # Git timeout to read a commit, in seconds - timeout: 10 ## Webpack settings # If enabled, this will tell rails to serve frontend assets from the webpack-dev-server running diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 94429ee91a9..27c1ecc7b23 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -499,9 +499,7 @@ Settings.backup['upload']['storage_class'] ||= nil # Git # Settings['git'] ||= Settingslogic.new({}) -Settings.git['max_size'] ||= 20971520 # 20.megabytes -Settings.git['bin_path'] ||= '/usr/bin/git' -Settings.git['timeout'] ||= 10 +Settings.git['bin_path'] ||= '/usr/bin/git' # Important: keep the satellites.path setting until GitLab 9.0 at # least. This setting is fed to 'rm -rf' in diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 21fe8d72459..8560d24526f 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -12,13 +12,18 @@ unless Sidekiq.server? config.lograge.logger = ActiveSupport::Logger.new(filename) # Add request parameters to log output config.lograge.custom_options = lambda do |event| - { + payload = { time: event.time.utc.iso8601(3), params: event.payload[:params].except(*%w(controller action format)), remote_ip: event.payload[:remote_ip], user_id: event.payload[:user_id], username: event.payload[:username] } + + gitaly_calls = Gitlab::GitalyClient.get_request_count + payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0 + + payload end end end diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb index 820cc89ef57..c2f3023b330 100644 --- a/config/initializers/postgresql_opclasses_support.rb +++ b/config/initializers/postgresql_opclasses_support.rb @@ -127,7 +127,7 @@ module ActiveRecord orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} where = inddef.scan(/WHERE (.+)$/).flatten[0] using = inddef.scan(/USING (.+?) /).flatten[0].to_sym - opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass| + opclasses = Hash[inddef.scan(/\((.+?)\)(?:$| WHERE )/).flatten[0].split(',').map do |column_and_opclass| column, opclass = column_and_opclass.split(' ').map(&:strip) [column, opclass] if opclass end.compact] diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 24c001362c6..8235e3853dc 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -38,7 +38,6 @@ - [invalid_gpg_signature_update, 2] - [create_gpg_signature, 2] - [upload_checksum, 1] - - [use_key, 1] - [repository_fork, 1] - [repository_import, 1] - [project_service, 1] @@ -63,3 +62,5 @@ - [update_user_activity, 1] - [propagate_service_template, 1] - [background_migration, 1] + - [project_migrate_hashed_storage, 1] + - [storage_migrator, 1] diff --git a/config/svg.config.js b/config/svg.config.js new file mode 100644 index 00000000000..be72741abec --- /dev/null +++ b/config/svg.config.js @@ -0,0 +1,48 @@ +/* eslint-disable no-commonjs */ +const path = require('path'); +const fs = require('fs'); + +const sourcePath = path.join('node_modules', 'gitlab-svgs', 'dist'); +const sourcePathIllustrations = path.join('node_modules', 'gitlab-svgs', 'dist', 'illustrations'); +const destPath = path.normalize(path.join('app', 'assets', 'images')); + +// Actual Task copying the 2 files + all illustrations +copyFileSync(path.join(sourcePath, 'icons.svg'), destPath); +copyFileSync(path.join(sourcePath, 'icons.json'), destPath); +copyFolderRecursiveSync(sourcePathIllustrations, destPath); + +// Helper Functions +function copyFileSync(source, target) { + var targetFile = target; + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if (fs.lstatSync(target).isDirectory()) { + targetFile = path.join(target, path.basename(source)); + } + } + console.log(`Copy SVG File : ${targetFile}`); + fs.writeFileSync(targetFile, fs.readFileSync(source)); +} + +function copyFolderRecursiveSync(source, target) { + var files = []; + + //check if folder needs to be created or integrated + var targetFolder = path.join(target, path.basename(source)); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + var curSource = path.join(source, file); + if (fs.lstatSync(curSource).isDirectory()) { + copyFolderRecursiveSync(curSource, targetFolder); + } else { + copyFileSync(curSource, targetFolder); + } + }); + } +} diff --git a/config/webpack.config.js b/config/webpack.config.js index 6b0cd023291..3404715fe30 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -45,6 +45,7 @@ var config = { group: './group.js', groups: './groups/index.js', groups_list: './groups_list.js', + help: './help/help.js', how_to_merge: './how_to_merge.js', issue_show: './issue_show/index.js', integrations: './integrations', diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 6553c5d457a..1f8f5cfc82b 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -4,9 +4,9 @@ Sidekiq::Testing.inline! do Gitlab::Seeder.quiet do project_urls = [ 'https://gitlab.com/gitlab-org/gitlab-test.git', - 'https://gitlab.com/gitlab-org/gitlab-ce.git', - 'https://gitlab.com/gitlab-org/gitlab-ci.git', 'https://gitlab.com/gitlab-org/gitlab-shell.git', + 'https://gitlab.com/gnuwget/wget2.git', + 'https://gitlab.com/Commit451/LabCoat.git', 'https://github.com/documentcloud/underscore.git', 'https://github.com/twitter/flight.git', 'https://github.com/twitter/typeahead.js.git', diff --git a/db/migrate/20160713200638_add_repository_read_only_to_projects.rb b/db/migrate/20160713200638_add_repository_read_only_to_projects.rb new file mode 100644 index 00000000000..8ee8b55f210 --- /dev/null +++ b/db/migrate/20160713200638_add_repository_read_only_to_projects.rb @@ -0,0 +1,9 @@ +class AddRepositoryReadOnlyToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :projects, :repository_read_only, :boolean + end +end diff --git a/db/migrate/20170720122741_create_user_custom_attributes.rb b/db/migrate/20170720122741_create_user_custom_attributes.rb new file mode 100644 index 00000000000..b1c0bebc633 --- /dev/null +++ b/db/migrate/20170720122741_create_user_custom_attributes.rb @@ -0,0 +1,17 @@ +class CreateUserCustomAttributes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :user_custom_attributes do |t| + t.timestamps_with_timezone null: false + t.references :user, null: false, foreign_key: { on_delete: :cascade } + t.string :key, null: false + t.string :value, null: false + + t.index [:user_id, :key], unique: true + t.index [:key, :value] + end + end +end diff --git a/db/migrate/20170828135939_migrate_user_external_mail_data.rb b/db/migrate/20170828135939_migrate_user_external_mail_data.rb index 592e141b7e6..f7ac87374b6 100644 --- a/db/migrate/20170828135939_migrate_user_external_mail_data.rb +++ b/db/migrate/20170828135939_migrate_user_external_mail_data.rb @@ -33,7 +33,7 @@ class MigrateUserExternalMailData < ActiveRecord::Migration SELECT true FROM user_synced_attributes_metadata WHERE user_id = users.id - AND provider = users.email_provider + AND (provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL)) ) AND id BETWEEN #{start_id} AND #{end_id} EOF diff --git a/db/migrate/20170912113435_clean_stages_statuses_migration.rb b/db/migrate/20170912113435_clean_stages_statuses_migration.rb new file mode 100644 index 00000000000..fc091d7894e --- /dev/null +++ b/db/migrate/20170912113435_clean_stages_statuses_migration.rb @@ -0,0 +1,26 @@ +class CleanStagesStatusesMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class Stage < ActiveRecord::Base + include ::EachBatch + self.table_name = 'ci_stages' + end + + def up + Gitlab::BackgroundMigration.steal('MigrateStageStatus') + + Stage.where('status IS NULL').each_batch(of: 50) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::MigrateStageStatus.new.perform(*range) + end + end + + def down + # noop + end +end diff --git a/db/migrate/20170918222253_reorganize_deployments_indexes.rb b/db/migrate/20170918222253_reorganize_deployments_indexes.rb new file mode 100644 index 00000000000..139427ed2b9 --- /dev/null +++ b/db/migrate/20170918222253_reorganize_deployments_indexes.rb @@ -0,0 +1,28 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ReorganizeDeploymentsIndexes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_index_if_not_exists :deployments, [:environment_id, :iid, :project_id] + remove_index_if_exists :deployments, [:project_id, :environment_id, :iid] + end + + def down + add_index_if_not_exists :deployments, [:project_id, :environment_id, :iid] + remove_index_if_exists :deployments, [:environment_id, :iid, :project_id] + end + + def add_index_if_not_exists(table, columns) + add_concurrent_index(table, columns) unless index_exists?(table, columns) + end + + def remove_index_if_exists(table, columns) + remove_concurrent_index(table, columns) if index_exists?(table, columns) + end +end diff --git a/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb b/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb new file mode 100644 index 00000000000..b91efb86d98 --- /dev/null +++ b/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddDeploymentsIndexForLastDeployment < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + TO_INDEX = [:deployments, %i[environment_id id]].freeze + + def up + add_concurrent_index(*TO_INDEX) + end + + def down + remove_concurrent_index(*TO_INDEX) + end +end diff --git a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb new file mode 100644 index 00000000000..b2009b282e9 --- /dev/null +++ b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb @@ -0,0 +1,27 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveTemporaryCiBuildsIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # To use create/remove index concurrently + disable_ddl_transaction! + + def up + return unless index_exists?(:ci_builds, :id, name: 'index_for_ci_builds_retried_migration') + remove_concurrent_index(:ci_builds, :id, name: "index_for_ci_builds_retried_migration") + end + + def down + # this was a temporary index for a migration that was never + # present previously so this probably shouldn't be here but it's + # easier to test the drop if we have a way to create it. + add_concurrent_index("ci_builds", ["id"], + name: "index_for_ci_builds_retried_migration", + where: "(retried IS NULL)", + using: :btree) + end +end diff --git a/db/migrate/20170921115009_add_project_repository_storage_index.rb b/db/migrate/20170921115009_add_project_repository_storage_index.rb new file mode 100644 index 00000000000..1c5a8fd65e1 --- /dev/null +++ b/db/migrate/20170921115009_add_project_repository_storage_index.rb @@ -0,0 +1,19 @@ +class AddProjectRepositoryStorageIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(*index_spec) unless index_exists?(*index_spec) + end + + def down + remove_concurrent_index(*index_spec) if index_exists?(*index_spec) + end + + def index_spec + [:projects, :repository_storage] + end +end diff --git a/db/migrate/20170928100231_add_composite_index_on_merge_requests_merge_commit_sha.rb b/db/migrate/20170928100231_add_composite_index_on_merge_requests_merge_commit_sha.rb new file mode 100644 index 00000000000..9f02daf04c1 --- /dev/null +++ b/db/migrate/20170928100231_add_composite_index_on_merge_requests_merge_commit_sha.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddCompositeIndexOnMergeRequestsMergeCommitSha < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # The default index name is too long for PostgreSQL and would thus be + # truncated. + INDEX_NAME = 'index_merge_requests_on_tp_id_and_merge_commit_sha_and_id' + + COLUMNS = [:target_project_id, :merge_commit_sha, :id] + + disable_ddl_transaction! + + def up + return if index_is_present? + + add_concurrent_index(:merge_requests, COLUMNS, name: INDEX_NAME) + end + + def down + return unless index_is_present? + + remove_concurrent_index(:merge_requests, COLUMNS, name: INDEX_NAME) + end + + def index_is_present? + index_exists?(:merge_requests, COLUMNS, name: INDEX_NAME) + end +end diff --git a/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb b/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb index fefd931e5d2..fd1437b07f5 100644 --- a/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb +++ b/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb @@ -33,7 +33,7 @@ class PostDeployMigrateUserExternalMailData < ActiveRecord::Migration SELECT true FROM user_synced_attributes_metadata WHERE user_id = users.id - AND provider = users.email_provider + AND (provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL)) ) AND id BETWEEN #{start_id} AND #{end_id} EOF diff --git a/db/post_migrate/20170926150348_schedule_merge_request_diff_migrations_take_two.rb b/db/post_migrate/20170926150348_schedule_merge_request_diff_migrations_take_two.rb new file mode 100644 index 00000000000..5732cb85ea5 --- /dev/null +++ b/db/post_migrate/20170926150348_schedule_merge_request_diff_migrations_take_two.rb @@ -0,0 +1,32 @@ +class ScheduleMergeRequestDiffMigrationsTakeTwo < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 500 + MIGRATION = 'DeserializeMergeRequestDiffsAndCommits' + DELAY_INTERVAL = 10.minutes + + disable_ddl_transaction! + + class MergeRequestDiff < ActiveRecord::Base + self.table_name = 'merge_request_diffs' + + include ::EachBatch + + default_scope { where('st_commits IS NOT NULL OR st_diffs IS NOT NULL') } + end + + # By this point, we assume ScheduleMergeRequestDiffMigrations - the first + # version of this - has already run. On GitLab.com, we have ~220k un-migrated + # rows, but these rows will, in general, take a long time. + # + # With a gap of 10 minutes per batch, and 500 rows per batch, these migrations + # are scheduled over 220_000 / 500 / 6 ~= 74 hours, which is a little over + # three days. + def up + queue_background_migration_jobs_by_range_at_intervals(MergeRequestDiff, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + end +end diff --git a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb new file mode 100644 index 00000000000..a238216253b --- /dev/null +++ b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb @@ -0,0 +1,16 @@ +class UpdateLegacyDiffNotesTypeForImport < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + update_column_in_batches(:notes, :type, 'LegacyDiffNote') do |table, query| + query.where(table[:type].eq('Github::Import::LegacyDiffNote')) + end + end + + def down + end +end diff --git a/db/post_migrate/20170927112319_update_notes_type_for_import.rb b/db/post_migrate/20170927112319_update_notes_type_for_import.rb new file mode 100644 index 00000000000..1e70acd9868 --- /dev/null +++ b/db/post_migrate/20170927112319_update_notes_type_for_import.rb @@ -0,0 +1,16 @@ +class UpdateNotesTypeForImport < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + update_column_in_batches(:notes, :type, 'Note') do |table, query| + query.where(table[:type].eq('Github::Import::Note')) + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 2d8c33591f0..e8e64b9d36b 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: 20170914135630) do +ActiveRecord::Schema.define(version: 20170928100231) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -256,7 +256,6 @@ ActiveRecord::Schema.define(version: 20170914135630) do add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree - add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree, opclasses: {"id)"=>"WHERE"} add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree @@ -506,7 +505,8 @@ ActiveRecord::Schema.define(version: 20170914135630) do end add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree - add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree + add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree + add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree create_table "emails", force: :cascade do |t| @@ -895,6 +895,7 @@ ActiveRecord::Schema.define(version: 20170914135630) do add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree + add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} @@ -1215,6 +1216,7 @@ ActiveRecord::Schema.define(version: 20170914135630) do t.datetime "last_repository_updated_at" t.integer "storage_version", limit: 2 t.boolean "resolve_outdated_diff_discussions" + t.boolean "repository_read_only" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -1229,6 +1231,7 @@ ActiveRecord::Schema.define(version: 20170914135630) do add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree + add_index "projects", ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree @@ -1533,6 +1536,17 @@ ActiveRecord::Schema.define(version: 20170914135630) do add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree + create_table "user_custom_attributes", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.string "key", null: false + t.string "value", null: false + end + + add_index "user_custom_attributes", ["key", "value"], name: "index_user_custom_attributes_on_key_and_value", using: :btree + add_index "user_custom_attributes", ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true, using: :btree + create_table "user_synced_attributes_metadata", force: :cascade do |t| t.boolean "name_synced", default: false t.boolean "email_synced", default: false @@ -1759,6 +1773,7 @@ ActiveRecord::Schema.define(version: 20170914135630) do add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" + add_foreign_key "user_custom_attributes", "users", on_delete: :cascade add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md new file mode 100644 index 00000000000..bac8fa4bd9d --- /dev/null +++ b/doc/administration/raketasks/storage.md @@ -0,0 +1,107 @@ +# Repository Storage Rake Tasks + +This is a collection of rake tasks you can use to help you list and migrate +existing projects from Legacy storage to the new Hashed storage type. + +You can read more about the storage types [here][storage-types]. + +## List projects on Legacy storage + +To have a simple summary of projects using **Legacy** storage: + +**Omnibus Installation** + +```bash +gitlab-rake gitlab:storage:legacy_projects +``` + +**Source Installation** + +```bash +rake gitlab:storage:legacy_projects + +``` + +------ + +To list projects using **Legacy** storage: + +**Omnibus Installation** + +```bash +gitlab-rake gitlab:storage:list_legacy_projects +``` + +**Source Installation** + +```bash +rake gitlab:storage:list_legacy_projects + +``` + +## List projects on Hashed storage + +To have a simple summary of projects using **Hashed** storage: + +**Omnibus Installation** + +```bash +gitlab-rake gitlab:storage:hashed_projects +``` + +**Source Installation** + +```bash +rake gitlab:storage:hashed_projects + +``` + +------ + +To list projects using **Hashed** storage: + +**Omnibus Installation** + +```bash +gitlab-rake gitlab:storage:list_hashed_projects +``` + +**Source Installation** + +```bash +rake gitlab:storage:list_hashed_projects + +``` + +## Migrate existing projects to Hashed storage + +Before migrating your existing projects, you should +[enable hashed storage][storage-migration] for the new projects as well. + +This task will schedule all your existing projects to be migrated to the +**Hashed** storage type: + +**Omnibus Installation** + +```bash +gitlab-rake gitlab:storage:migrate_to_hashed +``` + +**Source Installation** + +```bash +rake gitlab:storage:migrate_to_hashed + +``` + +You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen. +There is a specific Queue you can watch to see how long it will take to finish: **project_migrate_hashed_storage** + +After it reaches zero, you can confirm every project has been migrated by running the commands above. +If you find it necessary, you can run this migration script again to schedule missing projects. + +Any error or warning will be logged in the sidekiq log file. + + +[storage-types]: ../repository_storage_types.md +[storage-migration]: ../repository_storage_types.md#how-to-migrate-to-hashed-storage diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md new file mode 100644 index 00000000000..fa882bbe28a --- /dev/null +++ b/doc/administration/repository_storage_types.md @@ -0,0 +1,69 @@ +# Repository Storage Types + +> [Introduced][ce-28283] in GitLab 10.0. + +## Legacy Storage + +Legacy Storage is the storage behavior prior to version 10.0. For historical reasons, GitLab replicated the same +mapping structure from the projects URLs: + + * Project's repository: `#{namespace}/#{project_name}.git` + * Project's wiki: `#{namespace}/#{project_name}.wiki.git` + +This structure made simple to migrate from existing solutions to GitLab and easy for Administrators to find where the +repository is stored. + +On the other hand this has some drawbacks: + +Storage location will concentrate huge amount of top-level namespaces. The impact can be reduced by the introduction of [multiple storage paths][storage-paths]. + +Because Backups are a snapshot of the same URL mapping, if you try to recover a very old backup, you need to verify +if any project has taken the place of an old removed project sharing the same URL. This means that `mygroup/myproject` +from your backup may not be the same original project that is today in the same URL. + +Any change in the URL will need to be reflected on disk (when groups / users or projects are renamed). This can add a lot +of load in big installations, and can be even worst if they are using any type of network based filesystem. + +Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct +order or we may end-up with wrong repository or missing data temporarily. + +## Hashed Storage + +Hashed Storage is the new storage behavior we are rolling out with 10.0. It's not enabled by default yet, but we +encourage everyone to try-it and take the time to fix any script you may have that depends on the old behavior. + +Instead of coupling project URL and the folder structure where the repository will be stored on disk, we are coupling +a hash, based on the project's ID. + +This makes the folder structure immutable, and therefore eliminates any requirement to synchronize state from URLs to +disk structure. This means that renaming a group, user or project will cost only the database transaction, and will take +effect immediately. + +The hash also helps to spread the repositories more evenly on the disk, so the top-level directory will contain less +folders than the total amount of top-level namespaces. + +Hash format is based on hexadecimal representation of SHA256: `SHA256(project.id)`. +Top-level folder uses first 2 characters, followed by another folder with the next 2 characters. They are both stored in +a special folder `@hashed`, to co-exist with existing Legacy projects: + +```ruby +# Project's repository: +"@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}.git" + +# Wiki's repository: +"@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}.wiki.git" +``` + +This new format also makes possible to restore backups with confidence, as when restoring a repository from the backup, +you will never mistakenly restore a repository in the wrong project (considering the backup is made after the migration). + +### How to migrate to Hashed Storage + +In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select +"_Create new projects using hashed storage paths_". + +To migrate your existing projects to the new storage type, check the specific [rake tasks]. + +[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283 +[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage +[storage-paths]: repository_storage_types.md diff --git a/doc/api/README.md b/doc/api/README.md index 6cbea29bda6..3fd4c97e536 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -14,6 +14,7 @@ following locations: - [Project-level Variables](project_level_variables.md) - [Group-level Variables](group_level_variables.md) - [Commits](commits.md) +- [Custom Attributes](custom_attributes.md) - [Deployments](deployments.md) - [Deploy Keys](deploy_keys.md) - [Environments](environments.md) diff --git a/doc/api/commits.md b/doc/api/commits.md index 2a78553782f..5a4a8d888b3 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -181,6 +181,12 @@ Example response: "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], + "last_pipeline" : { + "id": 8, + "ref": "master", + "sha": "2dc6aa325a317eda67812f05600bdf0fcdc70ab0" + "status": "created" + } "stats": { "additions": 15, "deletions": 10, diff --git a/doc/api/custom_attributes.md b/doc/api/custom_attributes.md new file mode 100644 index 00000000000..8b26f7093ab --- /dev/null +++ b/doc/api/custom_attributes.md @@ -0,0 +1,105 @@ +# Custom Attributes API + +Every API call to custom attributes must be authenticated as administrator. + +## List custom attributes + +Get all custom attributes on a user. + +``` +GET /users/:id/custom_attributes +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a user | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes +``` + +Example response: + +```json +[ + { + "key": "location", + "value": "Antarctica" + }, + { + "key": "role", + "value": "Developer" + } +] +``` + +## Single custom attribute + +Get a single custom attribute on a user. + +``` +GET /users/:id/custom_attributes/:key +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a user | +| `key` | string | yes | The key of the custom attribute | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes/location +``` + +Example response: + +```json +{ + "key": "location", + "value": "Antarctica" +} +``` + +## Set custom attribute + +Set a custom attribute on a user. The attribute will be updated if it already exists, +or newly created otherwise. + +``` +PUT /users/:id/custom_attributes/:key +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a user | +| `key` | string | yes | The key of the custom attribute | +| `value` | string | yes | The value of the custom attribute | + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "value=Greenland" https://gitlab.example.com/api/v4/users/42/custom_attributes/location +``` + +Example response: + +```json +{ + "key": "location", + "value": "Greenland" +} +``` + +## Delete custom attribute + +Delete a custom attribute on a user. + +``` +DELETE /users/:id/custom_attributes/:key +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a user | +| `key` | string | yes | The key of the custom attribute | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes/location +``` diff --git a/doc/api/jobs.md b/doc/api/jobs.md index d60c7c12881..e7060e154f4 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -336,7 +336,7 @@ Parameters | Attribute | Type | Required | Description | |-------------|---------|----------|-------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `ref_name` | string | yes | The ref from a repository | +| `ref_name` | string | yes | The ref from a repository (can only be branch or tag name, not HEAD or SHA) | | `job` | string | yes | The name of the job | Example request: diff --git a/doc/api/projects.md b/doc/api/projects.md index 3144220e588..07331d05231 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -635,6 +635,98 @@ POST /projects/:id/fork | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to | +## List Forks of a project + +>**Note:** This feature was introduced in GitLab 10.1 + +List the projects accessible to the calling user that have an established, forked relationship with the specified project + +``` +GET /projects/:id/forks +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `archived` | boolean | no | Limit by archived status | +| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | +| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | +| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Return list of projects matching the search criteria | +| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | +| `owned` | boolean | no | Limit by projects owned by the current user | +| `membership` | boolean | no | Limit by projects that the current user is a member of | +| `starred` | boolean | no | Limit by projects starred by the current user | +| `statistics` | boolean | no | Include project statistics | +| `with_issues_enabled` | boolean | no | Limit by enabled issues feature | +| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/forks" +``` + +Example responses: + +```json +[ + { + "id": 3, + "description": null, + "default_branch": "master", + "visibility": "internal", + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "jobs_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, + "container_registry_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", + "creator_id": 3, + "namespace": { + "id": 3, + "name": "Diaspora", + "path": "diaspora", + "kind": "group", + "full_path": "diaspora" + }, + "import_status": "none", + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 1, + "public_jobs": true, + "shared_with_groups": [], + "only_allow_merge_if_pipeline_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, + "request_access_enabled": false, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" + } + } +] +``` + ## Star a project Stars a given project. Returns status code `304` if the project is already starred. diff --git a/doc/api/users.md b/doc/api/users.md index 6d5db16b36a..1643c584244 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -154,6 +154,12 @@ You can search users by creation date time range with: GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060 ``` +You can filter by [custom attributes](custom_attributes.md) with: + +``` +GET /users?custom_attributes[key]=value&custom_attributes[other_key]=other_value +``` + ## Single user Get a single user. diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index f28c9791bee..4586caa457d 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -250,6 +250,8 @@ By default, when using `docker:dind`, Docker uses the `vfs` storage driver which copies the filesystem on every run. This is a very disk-intensive operation which can be avoided if a different driver is used, for example `overlay2`. +### Requirements + 1. Make sure a recent kernel is used, preferably `>= 4.2`. 1. Check whether the `overlay` module is loaded: @@ -271,14 +273,27 @@ which can be avoided if a different driver is used, for example `overlay2`. overlay ``` -1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`: +### Use driver per project - ``` - variables: - DOCKER_DRIVER: overlay2 - ``` - -> **Note:** +You can enable the driver for each project individually by editing the project's `.gitlab-ci.yml`: + +``` +variables: + DOCKER_DRIVER: overlay2 +``` + +### Use driver for every project + +To enable the driver for every project, you can set the environment variable for every build by adding `environment` in the `[[runners]]` section of `config.toml`: + +```toml +environment = ["DOCKER_DRIVER=overlay2"] +``` + +If you're running multiple Runners you will have to modify all configuration files. + +> **Notes:** +- More information about the Runner configuration is available in the [Runner documentation](https://docs.gitlab.com/runner/configuration/). - For more information about using OverlayFS with Docker, you can read [Use the OverlayFS storage driver](https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/). diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index f69d71a5c39..38bd0450a09 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -252,6 +252,8 @@ The `cache:key` variable can use any of the [predefined variables](../variables/ The default key is **default** across the project, therefore everything is shared between each pipelines and jobs by default, starting from GitLab 9.0. +>**Note:** The `cache:key` variable cannot contain the `/` character. + --- **Example configurations** @@ -276,7 +278,7 @@ To enable per-job and per-branch caching: ```yaml cache: - key: "$CI_JOB_NAME/$CI_COMMIT_REF_NAME" + key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" untracked: true ``` @@ -284,7 +286,7 @@ To enable per-branch and per-stage caching: ```yaml cache: - key: "$CI_JOB_STAGE/$CI_COMMIT_REF_NAME" + key: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME" untracked: true ``` @@ -293,7 +295,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace ```yaml cache: - key: "%CI_JOB_STAGE%/%CI_COMMIT_REF_NAME%" + key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%" untracked: true ``` @@ -302,7 +304,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace ```yaml cache: - key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME" + key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME" untracked: true ``` @@ -1568,6 +1570,11 @@ Read more on [GitLab Pages user documentation](../../user/project/pages/index.md Each instance of GitLab CI has an embedded debug tool called Lint. You can find the link under `/ci/lint` of your gitlab instance. +## Using reserved keywords + +If you get validation error when using specific values (e.g., `true` or `false`), +try to quote them, or change them to a different form (e.g., `/bin/true`). + ## Skipping jobs If your commit message contains `[ci skip]` or `[skip ci]`, using any diff --git a/doc/development/README.md b/doc/development/README.md index 3096d9f25f0..1448a4c0414 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -44,6 +44,7 @@ - [Building a package for testing purposes](build_test_package.md) - [Manage feature flags](feature_flags.md) - [View sent emails or preview mailers](emails.md) +- [Working with Gitaly](gitaly.md) ## Databases diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md index f83a60e49e8..5452b0e7a2f 100644 --- a/doc/development/background_migrations.md +++ b/doc/development/background_migrations.md @@ -215,14 +215,29 @@ same time will ensure that both existing and new data is migrated. In the next release we can remove the `after_commit` hooks and related code. We will also need to add a post-deployment migration that consumes any remaining -jobs. Such a migration would look like this: +jobs and manually run on any un-migrated rows. Such a migration would look like +this: ```ruby class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration disable_ddl_transaction! + class Service < ActiveRecord::Base + include ::EachBatch + + self.table_name = 'services' + end + def up + # This must be included Gitlab::BackgroundMigration.steal('ExtractServicesUrl') + + # This should be included, but can be skipped - see below + Service.where(url: nil).each_batch(of: 50) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::ExtractServicesUrl.new.perform(*range) + end end def down @@ -230,6 +245,15 @@ class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration end ``` +The final step runs for any un-migrated rows after all of the jobs have been +processed. This is in case a Sidekiq process running the background migrations +received SIGKILL, leading to the jobs being lost. (See +[more reliable Sidekiq queue][reliable-sidekiq] for more information.) + +If the application does not depend on the data being 100% migrated (for +instance, the data is advisory, and not mission-critical), then this final step +can be skipped. + This migration will then process any jobs for the ExtractServicesUrl migration and continue once all jobs have been processed. Once done you can safely remove the `services.properties` column. @@ -254,6 +278,9 @@ for more details. 1. Make sure that background migration jobs are idempotent. 1. Make sure that tests you write are not false positives. +1. Make sure that if the data being migrated is critical and cannot be lost, the + clean-up migration also checks the final state of the data before completing. [migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md [issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351 +[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791 diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 64a89976300..7165b8062a7 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -9,8 +9,18 @@ There are a few rules to get your merge request accepted: **approved by a [backend maintainer][projects]**. 1. If your merge request includes only frontend changes [^1], it must be **approved by a [frontend maintainer][projects]**. + 1. If your merge request includes UX changes [^1], it must + be **approved by a [UX team member][team]**. + 1. If your merge request includes adding a new JavaScript library [^1], it must be + **approved by a [frontend lead][team]**. + 1. If your merge request includes adding a new UI/UX paradigm [^1], it must be + **approved by a [UX lead][team]**. 1. If your merge request includes frontend and backend changes [^1], it must be **approved by a [frontend and a backend maintainer][projects]**. + 1. If your merge request includes UX and frontend changes [^1], it must + be **approved by a [UX team member and a frontend maintainer][team]**. + 1. If your merge request includes UX, frontend and backend changes [^1], it must + be **approved by a [UX team member, a frontend and a backend maintainer][team]**. 1. If your merge request includes a new dependency or a filesystem change, it must be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details. 1. To lower the amount of merge requests maintainers need to review, you can diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md new file mode 100644 index 00000000000..a76e978bd26 --- /dev/null +++ b/doc/development/fe_guide/icons.md @@ -0,0 +1,40 @@ +# Icons + +We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are only loaded once and then referenced through an ID. The sprite SVG is located under `/assets/icons.svg`. Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome usages. + +### Usage in HAML/Rails + +To use a sprite Icon in HAML or Rails we use a specific helper function : + +`sprite_icon(icon_name, size: nil, css_class: '')` + +**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`). +**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class) +**css_class (optional)** If you want to add additional css classes + +**Example** + +`= sprite_icon('issues', size: 72, css_class: 'icon-danger')` + +**Output from example above** + +`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>` + +### Usage in HTML/JS + +Please use the following function inside JS to render an icon : +`gl.utils.spriteIcon(iconName)` + +## Adding a new icon to the sprite + +All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. + +To upgrade to a new SVG Sprite version run `yarn upgrade https://gitlab.com/gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. + +# SVG Illustrations + +Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers. Please use the class `svg-content` around it to ensure nice rendering. The illustrations are also organised in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository (as they are then automatically optimised). + +**Example** + +`= image_tag 'illustrations/merge_requests.svg'` diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 64bcb4a0257..031b12a8e91 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -29,34 +29,6 @@ For our currently-supported browsers, see our [requirements][requirements]. ## Development Process -When you are assigned an issue please follow the next steps: - -### Divide a big feature into small Merge Requests -1. Big Merge Request are painful to review. In order to make this process easier we -must break a big feature into smaller ones and create a Merge Request for each step. -1. First step is to create a branch from `master`, let's call it `new-feature`. This branch -will be the recipient of all the smaller Merge Requests. Only this one will be merged to master. -1. Don't do any work on this one, let's keep it synced with master. -1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you -to clearly identify which step the branch represents. -1. Do the first part of the modifications in this branch. The target branch of this Merge Request -should be `new-feature`. -1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new -branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before. - -```shell -master -└─ new-feature - ├─ new-feature-step-1 - ├─ new-feature-step-2 - └─ new-feature-step-3 -``` - -**Tips** -- Make sure `new-feature` branch is always synced with `master`: merge master frequently. -- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*` -- Avoid rewriting history. - ### Share your work early 1. Before writing code guarantee your vision of the architecture is aligned with GitLab's architecture. @@ -98,6 +70,11 @@ Vue specific design patterns and practices. --- +## [Icons](icons.md) +How we use SVG for our Icons. + +--- + ## Style Guides ### [JavaScript Style Guide](style_guide_js.md) diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md new file mode 100644 index 00000000000..f0be3a6b141 --- /dev/null +++ b/doc/development/gitaly.md @@ -0,0 +1,54 @@ +# GitLab Developers Guide to Working with Gitaly + +[Gitaly](https://gitlab.com/gitlab-org/gitaly) is a high-level Git RPC service used by GitLab CE/EE, +Workhorse and GitLab-Shell. All Rugged operations in GitLab CE/EE are currently being phased out to +be replaced by Gitaly API calls. + +Visit the [Gitaly Migration Board](https://gitlab.com/gitlab-org/gitaly/boards/331341) for current +status of the migration. + +## Feature Flags + +Gitaly makes heavy use of [feature flags](feature_flags.md). + +Each Rugged-to-Gitaly migration goes through a [series of phases](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/MIGRATION_PROCESS.md): +* **Opt-In**: by default the Rugged implementation is used. + * Production instances can choose to enable the Gitaly endpoint by enabling the feature flag. + * For testing purposes, you may wish to enable all feature flags by default. This can be done by exporting the following + environment variable: `GITALY_FEATURE_DEFAULT_ON=1`. + * On developer instances (ie, when `Rails.env.development?` is true), the Gitaly endpoint + is enabled by default, but can be _disabled_ using feature flags. +* **Opt-Out**: by default, the Gitaly endpoint is used, but the feature can be explicitly disabled using the feature flag. +* **Madatory**: The migration is complete and cannot be disabled. The old codepath is removed. + +### Enabling and Disabling Feature + +In the Rails console, type: + +```ruby +Feature.enable(:gitaly_feature_name) +Feature.disable(:gitaly_feature_name) +``` + +Where `gitaly_feature_name` is the name of the Gitaly feature. This can be determined by finding the appropriate +`gitaly_migrate` code block, for example: + +```ruby +gitaly_migrate(:tag_names) do +... +end +``` + +Since Gitaly features are always prefixed with `gitaly_`, the name of the feature flag in this case would be `gitaly_tag_names`. + +## Gitaly-Related Test Failures + +If your test-suite is failing with Gitaly issues, as a first step, try running: + +```shell +rm -rf tmp/tests/gitaly +``` + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/img/manual_build_docs.png b/doc/development/img/manual_build_docs.png Binary files differindex fef767c2a79..615facabb5f 100644 --- a/doc/development/img/manual_build_docs.png +++ b/doc/development/img/manual_build_docs.png diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 9a5811d8474..a75cdf22f40 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -65,6 +65,7 @@ Libraries with the following licenses are unacceptable for use: - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. - [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU]. - [Facebook BSD + PATENTS][Facebook]: is a 3-clause BSD license with a patent grant that has been deemed [Category X][x-list] by the Apache foundation. +- [WTFPL][WTFPL]: is a public domain dedication [rejected by the OSI (3.2)][WTFPL-OSI]. Also has a strong language which is not in accordance with our diversity policy. ## Requesting Approval for Licenses @@ -108,3 +109,5 @@ Gems which are included only in the "development" or "test" groups by Bundler ar [x-list]: https://www.apache.org/legal/resolved.html#category-x [Acceptable-Licenses]: #acceptable-licenses [Unacceptable-Licenses]: #unacceptable-licenses +[WTFPL]: https://wtfpl.net +[WTFPL-OSI]: https://opensource.org/minutes20090304 diff --git a/doc/development/testing.md b/doc/development/testing.md index 83269303005..d856b003353 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -150,6 +150,16 @@ always in-sync with the codebase. [GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa [part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa +## Test for what should not be there + +This is particularly important for permission calls and might be called a +negative assertion: make sure only the bare minimum is returned and nothing else. + +See an issue about [leaking tokens] as an example of a vulnerability that is +captured by such a test. + +[leaking tokens]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37948 + ## How to test at the correct level? As many things in life, deciding what to test at each level of testing is a @@ -493,24 +503,24 @@ Here are some things to keep in mind regarding test performance: Our current CI parallelization setup is as follows: -1. The `knapsack` job in the prepare stage that is supposed to ensure we have a - `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file: +1. The `retrieve-tests-metadata` job in the `prepare` stage ensures that we have + a `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file: - The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched from S3, if it's not here we initialize the file with `{}`. -1. Each `rspec x y` job are run with `knapsack rspec` and should have an evenly - distributed share of tests: +1. Each `rspec-pg x y`/`rspec-mysql x y` job is run with `knapsack rspec` and + should have an evenly distributed share of tests: - It works because the jobs have access to the `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts from all previous stages are passed by default". [^1] - - the jobs set their own report path to + - The jobs set their own report path to `KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`. - - if knapsack is doing its job, test files that are run should be listed under + - If knapsack is doing its job, test files that are run should be listed under `Report specs`, not under `Leftover specs`. -1. The `update-knapsack` job takes all the +1. The `update-tests-metadata` job takes all the `knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json` - files from the `rspec x y` jobs and merge them all together into a single - `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that is then - uploaded to S3. + files from the `rspec-pg x y`/`rspec-mysql x y`jobs and merge them all together + into a single `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that + is then uploaded to S3. After that, the next pipeline will use the up-to-date `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index a436e9b1948..e215026bcca 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -32,19 +32,17 @@ This is the typeface used for code blocks and references to commits, branches, a --- ## Icons -GitLab uses Font Awesome icons throughout our interface. -| | | -| :-----------: | :---- | -| ![Trash icon](img/icon-trash.png) | The trash icon is used for destructive actions that deletes information. | -| ![Edit icon](img/icon-edit.png) | The pencil icon is used for editing content such as comments.| -| ![Notification icon](img/icon-notification.png) | The bell icon is for notifications, such as Todos. | -| ![Subscribe icon](img/icon-subscribe.png) | The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label. | -| ![RSS icon](img/icon-rss.png) | The standard RSS icon is used for linking to RSS/atom feeds. | -| ![Close icon](img/icon-close.png) | An 'x' is used for closing UI elements such as dropdowns. | -| ![Add icon](img/icon-add.png) | A plus is used when creating new objects, such as issues, projects, etc. | - -> TODO: update this section, add more general guidance to icon usage and personality, etc. +GitLab has a strong, unique personality. When you look at any screen, you should know immediately that it is GitLab. +Iconography is a powerful visual cue to the user and is a great way for us to reflect our particular sense of style. + +- **Standard size:** 16px * 16px +- **Border thickness:** 2px +- **Border radius:** 3px + +![Icon sampler](img/icon-spec.png) + +> TODO: List all icons, proper usage, hover, and active states. --- diff --git a/doc/development/ux_guide/illustrations.md b/doc/development/ux_guide/illustrations.md new file mode 100644 index 00000000000..7e16c300921 --- /dev/null +++ b/doc/development/ux_guide/illustrations.md @@ -0,0 +1,86 @@ +# Illustrations + +The illustrations should always align with topics and goals in specific context. + +## Principles + +#### Be simple. +- For clarity, we use simple and specific elements to create our illustrations. + +#### Be optimistic. +- We are an open-minded, optimistic, and friendly team. We should reflect those values in our illustrations to connect with our brand experience. + +#### Be gentle. +- Our illustrations assist users in understanding context and guide users in the right direction. Illustrations are supportive, so they should be obvious but not aggressive. + + +## Style + +#### Shapes +- All illustrations are geometric rather than organic. +- The illustrations are made by circles, rectangles, squares, and triangles. + +<img src="img/illustrations-geometric.png" width=224px alt="Example for geometric" /> + +#### Stroke +- Standard border thickness: **4px** +- Depending on the situation, border thickness can be changed to **3px**. For example, when the illustration size is small, an illustration with 4px border thickness would look tight. In this case, the border thickness can be changed to 3px. +- We use **rounded caps** and **rounded corner**. + +| Do | Don't | +| -------- | -------- | +| <img src="img/illustrations-caps-do.png" width= 133px alt="Do: caps and corner" /> | <img src="img/illustrations-caps-don't.png" width= 133px alt="Don't: caps and corner"/> | + +#### Radius +- Standard corner radius: **10px** +- Depending on the situation, corner radius can be changed to **5px**. For example, when the illustration size is small, an illustration with 10px corner radius would be over-rounded. In this case, the corner radius can be changed to 5px. + +<img src="img/illustrations-border-radius.png" width= 464px alt="Example for border radius"/> + +#### Size +Depends on the situation, the illustration size can be the 3 types below: + +**Large** +* Use case: Empty states, error pages(e.g. 404, 403) +* For vertical layout, the illustration should not larger than **430*300 px**. +* For horizontal layout, the illustration should not larger than **430*380 px**. + +| Vertical layout | Horizontal layout | +| --------------- | ----------------- | +| <img src="img/illustration-size-large-vertical.png" /> | <img src="img/illustration-size-large-horizontal.png" /> + +**Medium** +* Use case: Banner +* The illustration should not larger than **240*160 px** +* The illustration should keep simple and clear. We recommend not including too many elements in the medium size illustration. + +<img src="img/illustration-size-medium.png" width=983px /> + +**Small** +* Use case: Graphics for explanatory text, graphics for status. +* The illustration should not larger than **160*90 px**. +* The illustration should keep simple and clear. We recommend not including too many elements in the small size illustration. + +<img src="img/illustration-size-small.png" width=983px /> + +**Illustration on mobile** +- Keep the proportions in original ratio. + +#### Colors palette + +For consistency, we recommend choosing colors from our color palette. + +| Orange | Purple | Grey | +| -------- | -------- | -------- | +| <img src="img/illustrations-color-orange.png" width= 160px alt="Orange" /> | <img src="img/illustrations-color-purple.png" width= 160px alt="Purple" /> | <img src="img/illustrations-color-grey.png" width= 160px alt="Grey" /> | +| #FC6D26 | #6B4FBB | #EEEEEE | + +#### Don't +- Don't include the typography in the illustration. +- Don't include tanuki in the illustration. If necessary, we recommend having tanuki monochromatic. + +--- + +| Orange | Purple | +| -------- | -------- | +| <img src="img/illustrations-palette-oragne.png" width= 160px alt="Palette - Orange" /> | <img src="img/illustrations-palette-purple.png" width= 160px alt="Palette - Purple" /> | diff --git a/doc/development/ux_guide/img/icon-spec.png b/doc/development/ux_guide/img/icon-spec.png Binary files differnew file mode 100644 index 00000000000..56b19610dc1 --- /dev/null +++ b/doc/development/ux_guide/img/icon-spec.png diff --git a/doc/development/ux_guide/img/illustration-size-large-horizontal.png b/doc/development/ux_guide/img/illustration-size-large-horizontal.png Binary files differnew file mode 100755 index 00000000000..8aa835adccc --- /dev/null +++ b/doc/development/ux_guide/img/illustration-size-large-horizontal.png diff --git a/doc/development/ux_guide/img/illustration-size-large-vertical.png b/doc/development/ux_guide/img/illustration-size-large-vertical.png Binary files differnew file mode 100644 index 00000000000..813b6a065e5 --- /dev/null +++ b/doc/development/ux_guide/img/illustration-size-large-vertical.png diff --git a/doc/development/ux_guide/img/illustration-size-medium.png b/doc/development/ux_guide/img/illustration-size-medium.png Binary files differnew file mode 100755 index 00000000000..55cfe1dcb91 --- /dev/null +++ b/doc/development/ux_guide/img/illustration-size-medium.png diff --git a/doc/development/ux_guide/img/illustration-size-small.png b/doc/development/ux_guide/img/illustration-size-small.png Binary files differnew file mode 100644 index 00000000000..0124f58f48e --- /dev/null +++ b/doc/development/ux_guide/img/illustration-size-small.png diff --git a/doc/development/ux_guide/img/illustrations-border-radius.png b/doc/development/ux_guide/img/illustrations-border-radius.png Binary files differnew file mode 100755 index 00000000000..4e2fef5c7f5 --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-border-radius.png diff --git a/doc/development/ux_guide/img/illustrations-caps-do.png b/doc/development/ux_guide/img/illustrations-caps-do.png Binary files differnew file mode 100755 index 00000000000..7a2c74382f6 --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-caps-do.png diff --git a/doc/development/ux_guide/img/illustrations-caps-don't.png b/doc/development/ux_guide/img/illustrations-caps-don't.png Binary files differnew file mode 100755 index 00000000000..848f72dbe30 --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-caps-don't.png diff --git a/doc/development/ux_guide/img/illustrations-color-grey.png b/doc/development/ux_guide/img/illustrations-color-grey.png Binary files differnew file mode 100755 index 00000000000..63855026c2b --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-color-grey.png diff --git a/doc/development/ux_guide/img/illustrations-color-orange.png b/doc/development/ux_guide/img/illustrations-color-orange.png Binary files differnew file mode 100755 index 00000000000..96765c8c28c --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-color-orange.png diff --git a/doc/development/ux_guide/img/illustrations-color-purple.png b/doc/development/ux_guide/img/illustrations-color-purple.png Binary files differnew file mode 100755 index 00000000000..745d2c853ba --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-color-purple.png diff --git a/doc/development/ux_guide/img/illustrations-geometric.png b/doc/development/ux_guide/img/illustrations-geometric.png Binary files differnew file mode 100755 index 00000000000..33f05547bac --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-geometric.png diff --git a/doc/development/ux_guide/img/illustrations-palette-oragne.png b/doc/development/ux_guide/img/illustrations-palette-oragne.png Binary files differnew file mode 100755 index 00000000000..15f35912646 --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-palette-oragne.png diff --git a/doc/development/ux_guide/img/illustrations-palette-purple.png b/doc/development/ux_guide/img/illustrations-palette-purple.png Binary files differnew file mode 100755 index 00000000000..e0f5839705e --- /dev/null +++ b/doc/development/ux_guide/img/illustrations-palette-purple.png diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md index 8a849f239dc..42bcf234e12 100644 --- a/doc/development/ux_guide/index.md +++ b/doc/development/ux_guide/index.md @@ -21,6 +21,11 @@ Guidance on the timing, curving and motion for GitLab. --- +### [Illustrations](illustrations.md) +Guidelines for principals and styles related to illustrations for GitLab. + +--- + ### [Copy](copy.md) Conventions on text and messaging within labels, buttons, and other components. diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 479258f743e..68ba3dd2da3 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -1,6 +1,6 @@ # Writing documentation - - **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. + - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs). @@ -69,6 +69,51 @@ Use the [writing method](https://about.gitlab.com/handbook/product/technical-wri All the docs follow the same [styleguide](doc_styleguide.md). +### Contributing to docs + +Whenever a feature is changed, updated, introduced, or deprecated, the merge +request introducing these changes must be accompanied by the documentation +(either updating existing ones or creating new ones). This is also valid when +changes are introduced to the UI. + +The one resposible for writing the first piece of documentation is the developer who +wrote the code. It's the job of the Product Manager to ensure all features are +shipped with its docs, whether is a small or big change. At the pace GitLab evolves, +this is the only way to keep the docs up-to-date. If you have any questions about it, +please ask a Technical Writer. Otherwise, when your content is ready, assign one of +them to review it for you. + +We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything +is documented. + +### Feature overview and use cases + +Every major feature (regardless if present in GitLab Community or Enterprise editions) +should present, at the beginning of the document, two main sections: **overview** and +**use cases**. Every GitLab EE-only feature should also contain these sections. + +**Overview**: at the name suggests, the goal here is to provide an overview of the feature. +Describe what is it, what it does, why it is important/cool/nice-to-have, +what problem it solves, and what you can do with this feature that you couldn't +do before. + +**Use cases**: provide at least two, ideally three, use cases for every major feature. +You should answer this question: what can you do with this feature/change? Use cases +are examples of how this feauture or change can be used in real life. + +Examples: +- CE and EE: [Issues](../user/project/issues/index.md#use-cases) +- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview) +- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview) +- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview) + +Note that if you don't have anything to add between the doc title (`<h1>`) and +the header `## Overview`, you can omit the header, but keep the content of the +overview there. + +> **Overview** and **use cases** are required to **every** Enterprise Edition feature, +and for every **major** feature present in Community Edition. + ### Markdown Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. @@ -106,21 +151,84 @@ CE and EE. ## Previewing the changes live -If you want to preview your changes live, you can use the manual `build-docs` -job in your merge request. +If you want to preview the doc changes of your merge request live, you can use +the manual `review-docs-deploy` job in your merge request. + +TIP: **Tip:** +If your branch contains only documentation changes, you can use +[special branch names](#testing) to avoid long running pipelines. ![Manual trigger a docs build](img/manual_build_docs.png) This job will: 1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs) - project named after the scheme: `<CE/EE-branch-slug>-built-from-ce-ee` -1. Trigger a pipeline and build the docs site with your changes - -Look for the docs URL at the output of the `build-docs` job. - ->**Note:** + project named after the scheme: `preview-<branch-slug>` +1. Trigger a cross project pipeline and build the docs site with your changes + +After a few minutes, the Review App will be deployed and you will be able to +preview the changes. The docs URL can be found in two places: + +- In the merge request widget +- In the output of the `review-docs-deploy` job, which also includes the + triggered pipeline so that you can investigate whether something went wrong + +In case the Review App URL returns 404, follow these steps to debug: + +1. **Did you follow the URL from the merge request widget?** If yes, then check if + the link is the same as the one in the job output. It can happen that if the + branch name slug is longer than 35 characters, it is automatically + truncated. That means that the merge request widget will not show the proper + URL due to a limitation of how `environment: url` works, but you can find the + real URL from the output of the `review-docs-deploy` job. +1. **Did you follow the URL from the job output?** If yes, then it means that + either the site is not yet deployed or something went wrong with the remote + pipeline. Give it a few minutes and it should appear online, otherwise you + can check the status of the remote pipeline from the link in the job output. + If the pipeline failed or got stuck, drop a line in the `#docs` chat channel. + +TIP: **Tip:** +Someone that has no merge rights to the CE/EE projects (think of forks from +contributors) will not be able to run the manual job. In that case, you can +ask someone from the GitLab team who has the permissions to do that for you. + +NOTE: **Note:** Make sure that you always delete the branch of the merge request you were working on. If you don't, the remote docs branch won't be removed either, and the server where the Review Apps are hosted will eventually be out of disk space. + +### Behind the scenes + +If you want to know the hot details, here's what's really happening: + +1. You manually run the `review-docs-deploy` job in a CE/EE merge request. +1. The job runs the [`scirpts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs) + script with the `deploy` flag, which in turn: + 1. Takes your branch name and applies the following: + - The slug of the branch name is used to avoid special characters since + ultimately this will be used by NGINX. + - The `preview-` prefix is added to avoid conflicts if there's a remote branch + with the same name that you created in the merge request. + - The final branch name is truncated to 42 characters to avoid filesystem + limitations with long branch names (> 63 chars). + 1. The remote branch is then created if it doesn't exist (meaning you can + re-run the manual job as many times as you want and this step will be skipped). + 1. A new cross-project pipeline is triggered in the docs project. + 1. The preview URL is shown both at the job output and in the merge request + widget. You also get the link to the remote pipeline. +1. In the docs project, the pipeline is created and it + [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55) + to lower the build time. +1. Once the docs site is built, the HTML files are uploaded as artifacts. +1. A specific Runner tied only to the docs project, runs the Review App job + that downloads the artifacts and uses `rsync` to transfer the files over + to a location where NGINX serves them. + +The following GitLab features are used among others: + +- [Manual actions](../ci/yaml/README.md#manual-actions) +- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html) +- [Review Apps](../ci/review_apps/index.md) +- [Artifacts](../ci/yaml/README.md#artifacts) +- [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects) diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index 5c128f54a76..f9ba1508705 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -1,11 +1,12 @@ # Database MySQL >**Note:** -We do not recommend using MySQL due to various issues. For example, case +- We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](https://bugs.mysql.com/bug.php?id=65830) that [suggested](https://bugs.mysql.com/bug.php?id=50909) [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164). +- We recommend using MySQL version 5.6 or later. Please see the following [issue][ce-38152]. ## Initial database setup @@ -13,7 +14,7 @@ and [problems](https://bugs.mysql.com/bug.php?id=65830) that # Install the database packages sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev -# Ensure you have MySQL version 5.5.14 or later +# Ensure you have MySQL version 5.6 or later mysql --version # Pick a MySQL root password (can be anything), type it and press enter @@ -293,3 +294,4 @@ Details can be found in the [PostgreSQL][postgres-text-type] and [postgres-text-type]: http://www.postgresql.org/docs/9.2/static/datatype-character.html [mysql-text-types]: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html +[ce-38152]: https://gitlab.com/gitlab-org/gitlab-ce/issues/38152 diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 9d1280c3dc6..8c110a37380 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -148,32 +148,51 @@ helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,g ## Updating GitLab using the Helm Chart +>**Note**: If you are upgrading from a previous version to 0.1.35 or above, you will need to change the access mode values for GitLab's storage. To do this, set the following in `values.yaml` or on the CLI: +``` +gitlabDataAccessMode=ReadWriteMany +gitlabRegistryAccessMode=ReadWriteMany +gitlabConfigAccessMode=ReadWriteMany +``` + Once your GitLab Chart is installed, configuration changes and chart updates -should we done using `helm upgrade` +should be done using `helm upgrade`: ```bash -helm upgrade -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab +helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus ``` -where: +## Upgrading from CE to EE using the Helm Chart + +If you have installed the Community Edition using this chart, upgrading to Enterprise Edition is easy. -- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom - [configuration] (#configuring-and-installing-gitlab). -- `<RELEASE-NAME>` is the name you gave the chart when installing it. - In the [Install section](#installing-gitlab-using-the-helm-chart) we called it `gitlab`. +If you are using a `values.yaml` file to specify the configuration options, edit the file and set `gitlab=ee`. If you would like to run a specific version of GitLab EE, set `gitlabEEImage` to be the desired GitLab [docker image](https://hub.docker.com/r/gitlab/gitlab-ee/tags/). Then you can use `helm upgrade` to update your GitLab instance to EE: + +```bash +helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus +``` + +You can also upgrade and specify these options via the command line: + +```bash +helm upgrade gitlab --set gitlab=ee,gitlabEEImage=gitlab/gitlab-ee:9.5.5-ee.0 gitlab/gitlab-omnibus +``` ## Uninstalling GitLab using the Helm Chart To uninstall the GitLab Chart, run the following: ```bash -helm delete <RELEASE-NAME> +helm delete gitlab ``` -where: +## Troubleshooting + +### Storage errors when updating `gitlab-omnibus` versions prior to 0.1.35 + +Users upgrading `gitlab-omnibus` from a version prior to 0.1.35, may see an error like: `Error: UPGRADE FAILED: PersistentVolumeClaim "gitlab-gitlab-config-storage" is invalid: spec: Forbidden: field is immutable after creation`. -- `<RELEASE-NAME>` is the name you gave the chart when installing it. - In the [Install section](#installing) we called it `gitlab`. +This is due to a change in the access mode for GitLab storage in version 0.1.35. To successfully upgrade, the access mode flags must be set to `ReadWriteMany` as detailed in the [update section](#updating-gitlab-using-the-helm-chart). [kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types [storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses diff --git a/doc/install/requirements.md b/doc/install/requirements.md index f672b358096..17fe80fa93d 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -82,11 +82,11 @@ errors during usage. We recommend having at least 2GB of swap on your server, even if you currently have enough available RAM. Having swap will help reduce the chance of errors occurring -if your available memory changes. We also recommend [configuring the kernels swappiness setting](https://askubuntu.com/a/103916) +if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916) to a low value like `10` to make the most of your RAM while still having the swap available when needed. -Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. +Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. ## Database diff --git a/doc/integration/azure.md b/doc/integration/azure.md index 5e3e9f5ab77..f3c9c498634 100644 --- a/doc/integration/azure.md +++ b/doc/integration/azure.md @@ -74,6 +74,9 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap tenant_id: "TENANT ID" } } ``` + The `base_azure_url` is optional and can be added for different locales; + e.g. `base_azure_url: "https://login.microsoftonline.de"`. + 1. Replace 'CLIENT ID', 'CLIENT SECRET' and 'TENANT ID' with the values you got above. 1. Save the configuration file. diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png Binary files differdeleted file mode 100644 index 57bd7650a30..00000000000 --- a/doc/topics/autodevops/img/auto_devops_settings.png +++ /dev/null diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png Binary files differindex 5661b50841b..92902e3ca72 100644 --- a/doc/topics/autodevops/img/auto_monitoring.png +++ b/doc/topics/autodevops/img/auto_monitoring.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index b31b8eaaca0..5561784ed0b 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -1,73 +1,51 @@ # Auto DevOps -> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and -**not recommended for production use**. Access to the Container Registry is only -available while the pipeline is running. Restarting a pod, scaling a service, or -other actions which require on-going access **will fail** even for public -projects. On-going secure access is planned for a subsequent release. - -Auto DevOps brings best practices to your project in an easy and default way. A -typical web project starts with Continuous Integration (CI), then adds automated -deployment to production, and maybe some time in the future adds some kind of -monitoring. With Auto DevOps, every project has a complete workflow, with -no configuration, including: - -- [Auto Build](#auto-build) -- [Auto Test](#auto-test) -- [Auto Code Quality](#auto-code-quality) -- [Auto Review Apps](#auto-review-apps) -- [Auto Deploy](#auto-deploy) -- [Auto Monitoring](#-auto-monitoring) +DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_. + +> [Introduced][ce-37115] in GitLab 10.0. + +Auto DevOps automatically detects, builds, tests, deploys, and monitors your +applications. ## Overview -You will need [Kubernetes](https://kubernetes.io/) and -[Prometheus](https://prometheus.io/) to make full use of Auto DevOps, but -even projects using only [GitLab Runners](https://docs.gitlab.com/runner/) will -be able to make use of Auto Build, Auto Test, and Auto Code Quality. - -Auto DevOps makes use of an open source tool called -[Herokuish](https://github.com/gliderlabs/herokuish) which uses [Heroku -buildpacks](https://devcenter.heroku.com/articles/buildpacks) to automatically -detect, build, and test applications. Auto DevOps supports all of the languages -and frameworks that are [supported by -Herokuish](https://github.com/gliderlabs/herokuish#buildpacks) such as Ruby, -Rails, Node, PHP, Python, and Java, and [custom buildpacks can be -specified](#using-custom-buildpacks). *GitLab is in no way affiliated with Heroku -or Glider Labs.* - -Projects can [customize](#customizing) the process by specifying [custom -buildpacks](#custom-buildpack), [custom `Dockerfile`s](#custom-dockerfile), -[custom Helm charts](#custom-helm-chart), or even copying the complete CI/CD -configuration into your project to enable staging and canary deployments, and -more. +With Auto DevOps, the software development process becomes easier to set up +as every project can have a complete workflow from build to deploy and monitoring, +with minimal to zero configuration. -## Quick start +Comprised of a set of stages, Auto DevOps brings these best practices to your +project in an easy and automatic way: -If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) -for using Auto DevOps with GitLab.com and an external Kubernetes cluster on -Google Cloud. +1. [Auto Build](#auto-build) +1. [Auto Test](#auto-test) +1. [Auto Code Quality](#auto-code-quality) +1. [Auto Review Apps](#auto-review-apps) +1. [Auto Deploy](#auto-deploy) +1. [Auto Monitoring](#auto-monitoring) -For self-hosted installations, the easiest way to make use of Auto DevOps is to -install GitLab inside a Kubernetes cluster using the [GitLab-Omnibus Helm -Chart](../../install/kubernetes/gitlab_omnibus.md) which automatically installs -and configures everything you need. +As Auto DevOps relies on many different components, it's good to have a basic +knowledge of the following: -## Prerequisites +- [Kubernetes](https://kubernetes.io/docs/home/) +- [Helm](https://docs.helm.sh/) +- [Docker](https://docs.docker.com) +- [GitLab Runner](https://docs.gitlab.com/runner/) +- [Prometheus](https://prometheus.io/docs/introduction/overview/) -You will need one or more GitLab Runners, a Kubernetes cluster, and Prometheus -installed in the cluster to make full use of Auto DevOps. If you do not have -Kubernetes or Prometheus installed then Auto Review Apps, Auto Deploy, and Auto -Monitoring will be silently skipped. +Auto DevOps provides great defaults for all the stages; you can, however, +[customize](#customizing) almost everything to your needs. -If you are using GitLab outside of Kubernetes, for example with GitLab.com, then -you should take these prerequisites into account: +## Prerequisites + +TIP: **Tip:** +For self-hosted installations, the easiest way to make use of Auto DevOps is to +install GitLab inside a Kubernetes cluster using the [GitLab Omnibus Helm Chart] +which automatically installs and configures everything you need! -1. **Base domain** - You will need a base domain configured with wildcard DNS to - be used by all of your Auto DevOps applications. +To make full use of Auto DevOps, you will need: -1. **GitLab Runner** - Your Runner needs to be configured to be able to run Docker. - Generally this means using the +1. **GitLab Runner** (needed for all stages) - Your Runner needs to be + configured to be able to run Docker. Generally this means using the [Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode). @@ -78,35 +56,98 @@ you should take these prerequisites into account: should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner) for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner) that are assigned to specific projects. - -1. **Kubernetes** - To enable deploys, you will need Kubernetes 1.5+, with NGINX - ingress and wildcard SSL termination, for example using the - [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) - and [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego) - Helm charts respectively. The [Kubernetes service][kubernetes-service] +1. **Base domain** (needed for Auto Review Apps and Auto Deploy) - You will need + a domain configured with wildcard DNS which is gonna be used by all of your + Auto DevOps applications. [Read the specifics](#auto-devops-base-domain). +1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) - + To enable deployments, you will need Kubernetes 1.5+. The [Kubernetes service][kubernetes-service] integration will need to be enabled for the project, or enabled as a [default service template](../../user/project/integrations/services_templates.md) for the entire GitLab installation. - -1. **Prometheus** - To enable Auto Monitoring, you will need Prometheus installed - somewhere (inside or outside your cluster) and configured to scrape your - Kubernetes cluster. To get response metrics (in addition to system metrics), - you need to [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics). + 1. **A load balancer** - You can use NGINX ingress by deploying it to your + Kubernetes cluster using the + [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) + Helm chart. + 1. **Wildcard TLS termination** - You can deploy the + [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego) + Helm chart to your Kubernetes cluster to automatically issue certificates + for your domains using Let's Encrypt. +1. **Prometheus** (needed for Auto Monitoring) - To enable Auto Monitoring, you + will need Prometheus installed somewhere (inside or outside your cluster) and + configured to scrape your Kubernetes cluster. To get response metrics + (in addition to system metrics), you need to + [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics). The [Prometheus service](../../user/project/integrations/prometheus.md) integration needs to be enabled for the project, or enabled as a [default service template](../../user/project/integrations/services_templates.md) for the entire GitLab installation. -## Enabling Auto DevOps +NOTE: **Note:** +If you do not have Kubernetes or Prometheus installed, then Auto Review Apps, +Auto Deploy, and Auto Monitoring will be silently skipped. + +### Auto DevOps base domain + +The Auto DevOps base domain is required if you want to make use of [Auto +Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined +under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops). +It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`. + +A wildcard DNS A record matching the base domain is required, for example, +given a base domain of `example.com`, you'd need a DNS entry like: + +``` +*.example.com 3600 A 1.2.3.4 +``` + +where `example.com` is the domain name under which the deployed apps will be served, +and `1.2.3.4` is the IP address of your load balancer; generally NGINX +([see prerequisites](#prerequisites)). How to set up the DNS record is beyond +the scope of this document; you should check with your DNS provider. + +Once set up, all requests will hit the load balancer, which in turn will route +them to the Kubernetes pods that run your application(s). -In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps -section. Select "Enable Auto DevOps", add in your base domain, and save. +NOTE: **Note:** +If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two +options: provide a static IP, or have one assigned. For more information see the +relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites). -![auto devops settings](img/auto_devops_settings.png) +## Quick start + +If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) +for using Auto DevOps with GitLab.com and an external Kubernetes cluster on +Google Cloud. + +## Enabling Auto DevOps + +NOTE: **Note:** +If you haven't done already, read the [prerequisites](#prerequisites) to make +full use of Auto DevOps. If this is your fist time, we recommend you follow the +[quick start guide](#quick-start). + +1. Go to your project's **Settings > CI/CD > General pipelines settings** and + find the Auto DevOps section +1. Select "Enable Auto DevOps" +1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain) + that will be used by Kubernetes to deploy your application +1. Hit **Save changes** for the changes to take effect + +Now that it's enabled, there are a few more steps depending on whether your project +has a `.gitlab-ci.yml` or not: + +- **For projects with no `.gitlab-ci.yml` present:** + A pipeline needs to be triggered either by pushing a new commit to the + repository or manually visiting `https://example.gitlab.com/<username>/<project>/pipelines/new` + and creating a new pipeline for your default branch, generally `master`. +- **For projects with a `.gitlab-ci.yml` present:** + All you need to do is remove your existing `.gitlab-ci.yml`, and you can even + do that in a branch to test Auto DevOps before committing to `master`. ## Stages of Auto DevOps -The following sections describe the stages of Auto DevOps. +The following sections describe the stages of Auto DevOps. Read them carefully +to understand how each one works. ### Auto Build @@ -118,18 +159,24 @@ Auto Build creates a build of the application in one of two ways: to automatically detect and build the application into a Docker image. Either way, the resulting Docker image is automatically pushed to the -[Container Registry][container-registry], tagged with the commit SHA. +[Container Registry][container-registry] and tagged with the commit SHA. + +CAUTION: **Important:** +If you are also using Auto Review Apps and Auto Deploy and choose to provide +your own `Dockerfile`, make sure you expose your application to port +`5000` as this is the port assumed by the default Helm chart. ### Auto Test -Auto Test automatically tests your application using +Auto Test automatically runs the appropriate tests for your application using [Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku -buildpacks](https://devcenter.heroku.com/articles/buildpacks). Auto Test will -analyze your project to detect the language and framework, and run appropriate -tests. Several languages and frameworks are detected automatically, but if your -language is not detected, you may succeed with a [custom -buildpack](#custom-buildpack). +buildpacks](https://devcenter.heroku.com/articles/buildpacks) by analyzing +your project to detect the language and framework. Several languages and +frameworks are detected automatically, but if your language is not detected, +you may succeed with a [custom buildpack](#custom-buildpacks). Check the +[currently supported languages](#currently-supported-languages). +NOTE: **Note:** Auto Test uses tests you already have in your application. If there are no tests, it's up to you to add them. @@ -137,99 +184,110 @@ tests, it's up to you to add them. Auto Code Quality uses the open source [`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run -static analysis and other code checks on the current code, creating a report -that is uploaded as an artifact. In GitLab EE, differences between the source -and target branches are shown in the merge request widget. *GitLab is in no way -affiliated with Code Climate.* +static analysis and other code checks on the current code. The report is +created, and is uploaded as an artifact which you can later download and check +out. In GitLab Enterprise Edition Starter, differences between the source and +target branches are +[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). ### Auto Review Apps -Auto Review Apps create a [Review App][review-app] for each branch. Review Apps -are temporary application environments based on the branch's code so developers, -designers, QA, product managers, and other reviewers can actually see and -interact with code changes as part of the review process. - -The review app will have a unique URL based on the project name, the branch +NOTE: **Note:** +This is an optional step, since many projects do not have a Kubernetes cluster +available. If the [prerequisites](#prerequisites) are not met, the job will +silently be skipped. + +CAUTION: **Caution:** +Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.) +This can cause confusion with Helm not detecting the change, and subsequent +deploys with Auto DevOps can undo your changes. Also, if you change something +and want to undo it by deploying again, Helm may not detect that anything changed +in the first place, and thus not realize that it needs to re-apply the old config. + +[Review Apps][review-app] are temporary application environments based on the +branch's code so developers, designers, QA, product managers, and other +reviewers can actually see and interact with code changes as part of the review +process. Auto Review Apps create a Review App for each branch. + +The Review App will have a unique URL based on the project name, the branch name, and a unique number, combined with the Auto DevOps base domain. For example, `user-project-branch-1234.example.com`. A link to the Review App shows up in the merge request widget for easy discovery. When the branch is deleted, for example after the merge request is merged, the Review App will automatically be deleted. -This is an optional step, since many projects do not have a Kubernetes cluster -available. If the Kubernetes service is not configured, or if the variable -`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto -DevOps setting), the job will silently be skipped. - ### Auto Deploy -After a branch or merge request is merged into `master`, Auto Deploy deploys the -application to a `production` environment in the Kubernetes cluster, with a -namespace based on the project name and unique project ID. e.g. `project-4321`. +NOTE: **Note:** This is an optional step, since many projects do not have a Kubernetes cluster -available. If the Kubernetes service is not configured, or if the variable -`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto -DevOps setting), the job will silently be skipped. +available. If the [prerequisites](#prerequisites) are not met, the job will +silently be skipped. + +CAUTION: **Caution:** +Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.) +This can cause confusion with Helm not detecting the change, and subsequent +deploys with Auto DevOps can undo your changes. Also, if you change something +and want to undo it by deploying again, Helm may not detect that anything changed +in the first place, and thus not realize that it needs to re-apply the old config. + +After a branch or merge request is merged into the project's default branch (usually +`master`), Auto Deploy deploys the application to a `production` environment in +the Kubernetes cluster, with a namespace based on the project name and unique +project ID, for example `project-4321`. Auto Deploy doesn't include deployments to staging or canary by default, but the -Auto DevOps template contains job definitions for these tasks if you want to +[Auto DevOps template] contains job definitions for these tasks if you want to enable them. +You can make use of [environment variables](#helm-chart-variables) to automatically +scale your pod replicas. + ### Auto Monitoring +NOTE: **Note:** +Check the [prerequisites](#prerequisites) for Auto Monitoring to make this stage +work. + Once your application is deployed, Auto Monitoring makes it possible to monitor your application's server and response metrics right out of the box. Auto Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to get system metrics such as CPU and memory usage directly from [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md), and response metrics such as HTTP error rates, latency, and throughput from the -[NGINX -server](../../user/project/integrations/prometheus_library/nginx_ingress.md). +[NGINX server](../../user/project/integrations/prometheus_library/nginx_ingress.md). -* Response Metrics: latency, throughput, error rate -* System Metrics: CPU utilization, memory utilization +The metrics include: -To view the metrics, open the [Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments). +- **Response Metrics:** latency, throughput, error rate +- **System Metrics:** CPU utilization, memory utilization -![Auto Metrics](img/auto_monitoring.png) - -### Configuring Auto Monitoring - -If GitLab has been deployed using the -[omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no +If GitLab has been deployed using the [GitLab Omnibus Helm Chart], no configuration is required. -If you have installed GitLab using a different method: +If you have installed GitLab using a different method, you need to: 1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster -1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml). -1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. - -## Customizing +1. If you would like response metrics, ensure you are running at least version + 0.9.0 of NGINX Ingress and + [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml). +1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) + the NGINX Ingress deployment to be scraped by Prometheus using + `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. -### PostgreSQL Database Support +To view the metrics, open the +[Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments). -In order to support applications that require a database, -[PostgreSQL][postgresql] is provisioned by default. Credentials to access the -database are preconfigured, but can be customized by setting the associated -[variables](#postgresql-variables). These credentials can be used for defining a -`DATABASE_URL` of the format: -`postgres://user:password@postgres-host:postgres-port/postgres-database`. - -PostgreSQL provisioning can be disabled by creating a project variable -`POSTGRES_ENABLED` set to `false`. - -#### PostgreSQL Variables +![Auto Metrics](img/auto_monitoring.png) -Any variables set at the project or group level will override variables set in -the CI/CD configuration. +## Customizing -1. `POSTGRES_ENABLED: "false"`: disable automatic deployment of PostgreSQL -1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL -1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL -1. `POSTGRES_DB: "my-database"`: use custom database name for PostgreSQL +While Auto DevOps provides great defaults to get you started, you can customize +almost everything to fit your needs; from custom [buildpacks](#custom-buildpacks), +to [`Dockerfile`s](#custom-dockerfile), [Helm charts](#custom-helm-chart), or +even copying the complete [CI/CD configuration](#customizing-gitlab-ci-yml) +into your project to enable staging and canary deployments, and more. -### Custom buildpack +### Custom buildpacks If the automatic buildpack detection fails for your project, or if you want to use a custom buildpack, you can override the buildpack using a project variable @@ -237,12 +295,12 @@ or a `.buildpack` file in your project: - **Project variable** - Create a project variable `BUILDPACK_URL` with the URL of the buildpack to use. - - **`.buildpack` file** - Add a file in your project's repo called `.buildpack` and add the URL of the buildpack to use on a line in the file. If you want to - use multiple buildpacks, you can enter them in, one on each line + use multiple buildpacks, you can enter them in, one on each line. - >**Note:** Using multiple buildpacks may break Auto Test. +CAUTION: **Caution:** +Using multiple buildpacks isn't yet supported by Auto DevOps. ### Custom `Dockerfile` @@ -253,37 +311,138 @@ Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/). ### Custom Helm Chart -Auto DevOps uses Helm to deploy your application to Kubernetes. You can override -the Helm chart used by bundling up a chart into your project repo or by -specifying a project variable. - -**Bundled chart** - If your project has a `chart` directory with a `Chart.yaml` -file in it, Auto DevOps will detect the chart and use it instead of the default -chart. This can be a great way to control exactly how your application is -deployed. +Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernetes. +You can override the Helm chart used by bundling up a chart into your project +repo or by specifying a project variable: -**Project variable** - Create a project variable `AUTO_DEVOPS_CHART` with the -URL of a custom chart to use. +- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml` + file in it, Auto DevOps will detect the chart and use it instead of the [default + one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). + This can be a great way to control exactly how your application is deployed. +- **Project variable** - Create a [project variable](../../ci/variables/README.md#secret-variables) + `AUTO_DEVOPS_CHART` with the URL of a custom chart to use. -### Enable staging, canaries, and more with custom `.gitlab-ci.yml` +### Customizing `.gitlab-ci.yml` If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the -Auto DevOps template into your project's repo and edit as you see fit. +[Auto DevOps template] into your project's repo and edit as you see fit. + +Assuming that your project is new or it doesn't have a `.gitlab-ci.yml` file +present: -From your project home page, click on the `Set up CI` button, or click on the `+` -button and `New file` and pick `.gitlab-ci.yml` as the template type, or view an -existing `.gitlab-ci.yml` file. Then select "Auto DevOps" from the template -dropdown. You will then be able to edit or add any jobs needed. +1. From your project home page, either click on the "Set up CI" button, or click + on the plus button and (`+`), then "New file" +1. Pick `.gitlab-ci.yml` as the template type +1. Select "Auto-DevOps" from the template dropdown +1. Edit the template or add any jobs needed +1. Give an appropriate commit message and hit "Commit changes" -For example, if you want deploys to go to a staging environment instead of -directly to a production environment, you can enable the `staging` job by +TIP: **Tip:** The Auto DevOps template includes useful comments to help you +customize it. For example, if you want deployments to go to a staging environment +instead of directly to a production one, you can enable the `staging` job by renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of the `production` job to turn it into a manual action instead of deploying automatically. +### PostgreSQL database support + +In order to support applications that require a database, +[PostgreSQL][postgresql] is provisioned by default. The credentials to access +the database are preconfigured, but can be customized by setting the associated +[variables](#environment-variables). These credentials can be used for defining a +`DATABASE_URL` of the format: + +```yaml +postgres://user:password@postgres-host:postgres-port/postgres-database +``` + +### Environment variables + +The following variables can be used for setting up the Auto DevOps domain, +providing a custom Helm chart, or scaling your application. PostgreSQL can be +also be customized, and you can easily use a [custom buildpack](#custom-buildpacks). + +| **Variable** | **Description** | +| ------------ | --------------- | +| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). | +| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). | +| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment; defaults to 1. | +| `CANARY_PRODUCTION_REPLICAS`| The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. | +| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. | +| `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. | +| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | +| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | +| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142`| + +TIP: **Tip:** +Set up the replica variables using a +[project variable](../../ci/variables/README.md#secret-variables) +and scale your application by just redeploying it! + +CAUTION: **Caution:** +You should *not* scale your application using Kubernetes directly. This can +cause confusion with Helm not detecting the change, and subsequent deploys with +Auto DevOps can undo your changes. + +#### Advanced replica variables setup + +Apart from the two replica-related variables for production mentioned above, +you can also use others for different environments. + +There's a very specific mapping between Kubernetes' label named `track`, +GitLab CI/CD environment names, and the replicas environment variable. +The general rule is: `TRACK_ENV_REPLICAS`. Where: + +- `TRACK`: The capitalized value of the `track` + [Kubernetes label](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) + in the Helm Chart app definition. If not set, it will not be taken into account + to the variable name. +- `ENV`: The capitalized environment name of the deploy job that is set in + `.gitlab-ci.yml`. + +That way, you can define your own `TRACK_ENV_REPLICAS` variables with which +you will be able to scale the pod's replicas easily. + +In the example below, the environment's name is `qa` which would result in +looking for the `QA_REPLICAS` environment variable: + +```yaml +QA testing: + stage: deploy + environment: + name: qa + script: + - deploy qa +``` + +If, in addition, there was also a `track: foo` defined in the application's Helm +chart, like: + +```yaml +replicaCount: 1 +image: + repository: gitlab.example.com/group/project + tag: stable + pullPolicy: Always + secrets: + - name: gitlab-registry +application: + track: foo + tier: web +service: + enabled: true + name: web + type: ClusterIP + url: http://my.host.com/ + externalPort: 5000 + internalPort: 5000 +``` + +then the environment variable would be `FOO_QA_REPLICAS`. + ## Currently supported languages ->**Note:** +NOTE: **Note:** Not all buildpacks support Auto Test yet, as it's a relatively new enhancement. All of Heroku's [officially supported languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages) @@ -294,22 +453,28 @@ multi-buildpack does not. As of GitLab 10.0, the supported buildpacks are: ``` -* heroku-buildpack-multi v1.0.0 -* heroku-buildpack-ruby v168 -* heroku-buildpack-nodejs v99 -* heroku-buildpack-clojure v77 -* heroku-buildpack-python v99 -* heroku-buildpack-java v53 -* heroku-buildpack-gradle v23 -* heroku-buildpack-scala v78 -* heroku-buildpack-play v26 -* heroku-buildpack-php v122 -* heroku-buildpack-go v72 -* heroku-buildpack-erlang fa17af9 -* buildpack-nginx v8 +- heroku-buildpack-multi v1.0.0 +- heroku-buildpack-ruby v168 +- heroku-buildpack-nodejs v99 +- heroku-buildpack-clojure v77 +- heroku-buildpack-python v99 +- heroku-buildpack-java v53 +- heroku-buildpack-gradle v23 +- heroku-buildpack-scala v78 +- heroku-buildpack-play v26 +- heroku-buildpack-php v122 +- heroku-buildpack-go v72 +- heroku-buildpack-erlang fa17af9 +- buildpack-nginx v8 ``` -## Private Project Support - Experimental +## Limitations + +The following restrictions apply. + +### Private project support + +CAUTION: **Caution:** Private project support in Auto DevOps is experimental. When a project has been marked as private, GitLab's [Container Registry][container-registry] requires authentication when downloading @@ -319,27 +484,10 @@ Authentication credentials will be valid while the pipeline is running, allowing for a successful initial deployment. After the pipeline completes, Kubernetes will no longer be able to access the -container registry. **Restarting a pod, scaling a service, or other actions which -require on-going access to the registry will fail**. On-going secure access is +Container Registry. **Restarting a pod, scaling a service, or other actions which +require on-going access to the registry may fail**. On-going secure access is planned for a subsequent release. -## Disable the banner instance wide - -If an administrater would like to disable the banners on an instance level, this -feature can be disabled either through the console: - -```basb -$ gitlab-rails console -[1] pry(main)> Feature.get(:auto_devops_banner_disabled).disable -=> true -``` - -Or through the HTTP API with the admin access token: - -``` -curl --data "value=true" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled -``` - ## Troubleshooting - Auto Build and Auto Test may fail in detecting your language/framework. There @@ -347,13 +495,36 @@ curl --data "value=true" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https:// key files the buildpack is looking for. For example, for ruby apps, you must have a `Gemfile` to be properly detected, even though it is possible to write a Ruby app without a `Gemfile`. Try specifying a [custom - buildpack](#custom-buildpack). + buildpack](#custom-buildpacks). - Auto Test may fail because of a mismatch between testing frameworks. In this case, you may need to customize your `.gitlab-ci.yml` with your test commands. +### Disable the banner instance wide + +If an administrator would like to disable the banners on an instance level, this +feature can be disabled either through the console: + +```sh +sudo gitlab-rails console +``` + +Then run: + +```ruby +Feature.get(:auto_devops_banner_disabled).enable +``` + +Or through the HTTP API with an admin access token: + +```sh +curl --data "value=true" --header "PRIVATE-TOKEN: private_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled +``` + [ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 [kubernetes-service]: ../../user/project/integrations/kubernetes.md [docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor [review-app]: ../../ci/review_apps/index.md [container-registry]: ../../user/project/container_registry.md [postgresql]: https://www.postgresql.org/ +[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml +[GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 564dd3222ac..ffe05519d7b 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -1,14 +1,15 @@ # Auto DevOps: quick start guide -> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and -**not recommended for production use**. +DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_. + +> [Introduced][ce-37115] in GitLab 10.0. This is a step-by-step guide to deploying a project hosted on GitLab.com to Google Cloud, using Auto DevOps. We made a minimal [Ruby -application](https://gitlab.com/gitlab-examples/minimal-ruby-app) to use as an -example for this guide. It contains two files: +application](https://gitlab.com/auto-devops-examples/minimal-ruby-app) to use +as an example for this guide. It contains two main files: * `server.rb` - our application. It will start an HTTP server on port 5000 and render "Hello, world!" @@ -113,11 +114,9 @@ assigned to the cluster IP. In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save. -![auto devops settings](img/auto_devops_settings.png) - Next, a pipeline needs to be triggered. Since the test project doesn't have a `.gitlab-ci.yml`, you need to either push a change to the repository or -manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/run`, +manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/new`, where `<username>` is your username. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md index a7db1f2e069..a7db1f2e069 100755..100644 --- a/doc/university/training/gitlab_flow.md +++ b/doc/university/training/gitlab_flow.md diff --git a/doc/university/training/index.md b/doc/university/training/index.md index 03179ff5a77..03179ff5a77 100755..100644 --- a/doc/university/training/index.md +++ b/doc/university/training/index.md diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md index 3ed601625cf..3ed601625cf 100755..100644 --- a/doc/university/training/topics/additional_resources.md +++ b/doc/university/training/topics/additional_resources.md diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md index e6e4fea9b51..e6e4fea9b51 100755..100644 --- a/doc/university/training/topics/agile_git.md +++ b/doc/university/training/topics/agile_git.md diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md index a60c4365e0c..a60c4365e0c 100755..100644 --- a/doc/university/training/topics/bisect.md +++ b/doc/university/training/topics/bisect.md diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md index af7a70a2818..af7a70a2818 100755..100644 --- a/doc/university/training/topics/cherry_picking.md +++ b/doc/university/training/topics/cherry_picking.md diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md index 8149379b36f..8149379b36f 100755..100644 --- a/doc/university/training/topics/env_setup.md +++ b/doc/university/training/topics/env_setup.md diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md index b65457728c0..b65457728c0 100755..100644 --- a/doc/university/training/topics/explore_gitlab.md +++ b/doc/university/training/topics/explore_gitlab.md diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md index 4b34406ea75..4b34406ea75 100755..100644 --- a/doc/university/training/topics/feature_branching.md +++ b/doc/university/training/topics/feature_branching.md diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md index ec7bb2631aa..ec7bb2631aa 100755..100644 --- a/doc/university/training/topics/getting_started.md +++ b/doc/university/training/topics/getting_started.md diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md index 9ffb4b9c859..9ffb4b9c859 100755..100644 --- a/doc/university/training/topics/git_add.md +++ b/doc/university/training/topics/git_add.md diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md index ca1ff29d93b..ca1ff29d93b 100755..100644 --- a/doc/university/training/topics/git_intro.md +++ b/doc/university/training/topics/git_intro.md diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md index 32ebceff491..32ebceff491 100755..100644 --- a/doc/university/training/topics/git_log.md +++ b/doc/university/training/topics/git_log.md diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md index 8e5d3baf959..8e5d3baf959 100755..100644 --- a/doc/university/training/topics/gitlab_flow.md +++ b/doc/university/training/topics/gitlab_flow.md diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md index 77807b3e7ef..77807b3e7ef 100755..100644 --- a/doc/university/training/topics/merge_conflicts.md +++ b/doc/university/training/topics/merge_conflicts.md diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md index 5b446f02f63..5b446f02f63 100755..100644 --- a/doc/university/training/topics/merge_requests.md +++ b/doc/university/training/topics/merge_requests.md diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md index cf647284604..cf647284604 100755..100644 --- a/doc/university/training/topics/rollback_commits.md +++ b/doc/university/training/topics/rollback_commits.md diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md index c1bdda32645..c1bdda32645 100755..100644 --- a/doc/university/training/topics/stash.md +++ b/doc/university/training/topics/stash.md diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md index 5d869af64c1..5d869af64c1 100755..100644 --- a/doc/university/training/topics/subtree.md +++ b/doc/university/training/topics/subtree.md diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md index e9607b5a875..e9607b5a875 100755..100644 --- a/doc/university/training/topics/tags.md +++ b/doc/university/training/topics/tags.md diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md index 17dbb64b9e6..17dbb64b9e6 100755..100644 --- a/doc/university/training/topics/unstage.md +++ b/doc/university/training/topics/unstage.md diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md index 9e38df26b6a..9e38df26b6a 100755..100644 --- a/doc/university/training/user_training.md +++ b/doc/university/training/user_training.md diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md index 70934f9960a..843fb4ce26b 100644 --- a/doc/user/admin_area/monitoring/health_check.md +++ b/doc/user/admin_area/monitoring/health_check.md @@ -18,7 +18,7 @@ traffic until the system is ready or restart the container as needed. To access monitoring resources, the client IP needs to be included in a whitelist. -[Read how to add IPs to a whitelist for the monitoring endpoints.][admin]. +[Read how to add IPs to a whitelist for the monitoring endpoints][admin]. ## Using the endpoint diff --git a/doc/user/group/index.md b/doc/user/group/index.md index db0242f1324..a1671f9dd91 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -188,6 +188,27 @@ Besides giving you the option to edit any settings you've previously set when [creating the group](#create-a-new-group), you can also access further configurations for your group. +#### Changing a group's path + +> **Note:** If you want to retain ownership over the original namespace and +protect the URL redirects, then instead of changing a group's path or renaming a +username, you can create a new group and transfer projects to it. + +Changing a group's path can have unintended side effects. + +* Existing web URLs for the group and anything under it (i.e. projects) will +redirect to the new URLs +* Existing Git remote URLs for projects under the group will no longer work, but +Git responses will show an error with the new remote URL +* The original namespace can be claimed again by any group or user, which will +destroy web redirects and Git remote warnings +* If you are vacating the path so it can be claimed by another group or user, +you may need to rename the group name as well since both names and paths must be +unique + +> It is currently not possible to rename a namespace if it contains a +project with container registry tags, because the project cannot be moved. + #### Enforce 2FA to group members Add a security layer to your group by diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index d2478aea4bd..161a3af9903 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -84,10 +84,13 @@ structure. a subgroup. For more information check the [permissions table][permissions]. - For a list of words that are not allowed to be used as group names see the [reserved names][reserved]. +- Users can always create subgroups if they are explicitly added as an Owner to + a parent group even if group creation is disabled by an administrator in their + settings. To create a subgroup: -1. In the group's dashboard go to the **Subgroups** page and click **Create subgroup**. +1. In the group's dashboard go to the **Subgroups** page and click **New subgroup**. ![Subgroups page](img/create_subgroup_button.png) @@ -100,9 +103,7 @@ To create a subgroup: 1. Click the **Create group** button and you will be taken to the new group's dashboard page. ---- - -You can follow the same process to create any subsequent groups. +Follow the same process to create any subsequent groups. ## Membership diff --git a/doc/user/markdown.md b/doc/user/markdown.md index b42b8f0a525..454988b9b80 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -596,6 +596,30 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd> </dl> +#### Details and Summary + +Content can be collapsed using HTML's [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) and [`<summary>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary) tags. This is especially useful for collapsing long logs so they take up less screen space. + +<p> +<details> +<summary>Click me to collapse/fold.</summary> +These details will remain hidden until expanded. + +<pre><code>PASTE LOGS HERE</code></pre> +</details> +</p> + +**Note:** Unfortunately Markdown is not supported inside these tags, as described by the [markdown specification](https://daringfireball.net/projects/markdown/syntax#html). You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting). + +```html +<details> +<summary>Click me to collapse/fold.</summary> +These details will remain hidden until expanded. + +<pre><code>PASTE LOGS HERE</code></pre> +</details> +``` + ### Horizontal Rule ``` diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 7d25970fcb1..5ebb88bf324 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -8,10 +8,27 @@ experience according to the best approach to their cases. Your `username` is a unique [`namespace`](../group/index.md#namespaces) related to your user ID. +### Changing your username + You can change your `username` from your -[profile settings](#profile-settings). To avoid breaking -paths when you change your `username`, we suggest you follow -[this procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom). +[profile settings](#profile-settings). + +> **Note:** If you want to retain ownership over the original namespace and +protect the URL redirects, then instead of changing your username, you can +create a new group and transfer projects to it. +Alternatively, you can follow [this detailed procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom). + +Changing your username can have unintended side effects. + +* Existing web URLs for the user and anything under it (i.e. projects) will +redirect to the new URLs +* Existing Git remote URLs for projects under the user will no longer work, but +Git responses will show an error with the new remote URL +* The original namespace can be claimed again by any group or user, which will +destroy any web redirects and Git remote warnings + +> It is currently not possible to rename a namespace if it contains a +project with container registry tags, because the project cannot be moved. ## User profile diff --git a/doc/user/project/integrations/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png Binary files differindex 349a2dc8456..e535e2b8d46 100644 --- a/doc/user/project/integrations/img/kubernetes_configuration.png +++ b/doc/user/project/integrations/img/kubernetes_configuration.png diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png Binary files differindex 917068d9398..917068d9398 100755..100644 --- a/doc/user/project/integrations/img/webhook_logs.png +++ b/doc/user/project/integrations/img/webhook_logs.png diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md index f4000523938..e9738b683f9 100644 --- a/doc/user/project/integrations/kubernetes.md +++ b/doc/user/project/integrations/kubernetes.md @@ -1,3 +1,7 @@ +--- +last_updated: 2017-09-25 +--- + # GitLab Kubernetes / OpenShift integration GitLab can be configured to interact with Kubernetes, or other systems using the @@ -6,62 +10,114 @@ Kubernetes API (such as OpenShift). Each project can be configured to connect to a different Kubernetes cluster, see the [configuration](#configuration) section. -If you have a single cluster that you want to use for all your projects, -you can pre-fill the settings page with a default template. To configure the -template, see the [Services Templates](services_templates.md) document. - ## Configuration Navigate to the [Integrations page](project_services.md#accessing-the-project-services) -of your project and select the **Kubernetes** service to configure it. +of your project and select the **Kubernetes** service to configure it. Fill in +all the needed parameters, check the "Active" checkbox and hit **Save changes** +for the changes to take effect. ![Kubernetes configuration settings](img/kubernetes_configuration.png) -The Kubernetes service takes the following arguments: - -1. API URL -1. Custom CA bundle -1. Kubernetes namespace -1. Service token - -The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes -exposes several APIs - we want the "base" URL that is common to all of them, -e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. - -GitLab authenticates against Kubernetes using service tokens, which are -scoped to a particular `namespace`. If you don't have a service token yet, -you can follow the -[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) -to create one. You can also view or create service tokens in the -[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit -`Config -> Secrets`. - -Fill in the service token and namespace according to the values you just got. -If the API is using a self-signed TLS certificate, you'll also need to include -the `ca.crt` contents as the `Custom CA bundle`. +The Kubernetes service takes the following parameters: + +- **API URL** - + It's the URL that GitLab uses to access the Kubernetes API. Kubernetes + exposes several APIs, we want the "base" URL that is common to all of them, + e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. +- **CA certificate** (optional) - + If the API is using a self-signed TLS certificate, you'll also need to include + the `ca.crt` contents here. +- **Project namespace** (optional) - The following apply: + - By default you don't have to fill it in; by leaving it blank, GitLab will + create one for you. + - Each project should have a unique namespace. + - The project namespace is not necessarily the namespace of the secret, if + you're using a secret with broader permissions, like the secret from `default`. + - You should **not** use `default` as the project namespace. + - If you or someone created a secret specifically for the project, usually + with limited permissions, the secret's namespace and project namespace may + be the same. +- **Token** - + GitLab authenticates against Kubernetes using service tokens, which are + scoped to a particular `namespace`. If you don't have a service token yet, + you can follow the + [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) + to create one. You can also view or create service tokens in the + [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config) + (under **Config > Secrets**). + +TIP: **Tip:** +If you have a single cluster that you want to use for all your projects, +you can pre-fill the settings page with a default template. To configure the +template, see [Services Templates](services_templates.md). ## Deployment variables -The Kubernetes service exposes following +The Kubernetes service exposes the following [deployment variables](../../../ci/variables/README.md#deployment-variables) in the -GitLab CI build environment: +GitLab CI/CD build environment: -- `KUBE_URL` - equal to the API URL -- `KUBE_TOKEN` +- `KUBE_URL` - Equal to the API URL. +- `KUBE_TOKEN` - The Kubernetes token. - `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. -- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path +- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path to a file containing PEM data. -- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data. -- `KUBECONFIG` - Path to a file containing kubeconfig for this deployment. CA bundle would be embedded if specified. +- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data. +- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment. + CA bundle would be embedded if specified. + +## What you can get with the Kubernetes integration + +Here's what you can do with GitLab if you enable the Kubernetes integration. + +### Deploy Boards (EEP) + +> Available in [GitLab Enterprise Edition Premium][ee]. -## Web terminals +GitLab's Deploy Boards offer a consolidated view of the current health and +status of each CI [environment](../../../ci/environments.md) running on Kubernetes, +displaying the status of the pods in the deployment. Developers and other +teammates can view the progress and status of a rollout, pod by pod, in the +workflow they already use without any need to access Kubernetes. ->**NOTE:** -Added in GitLab 8.15. You must be the project owner or have `master` permissions -to use terminals. Support is currently limited to the first container in the +[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) + +### Canary Deployments (EEP) + +> Available in [GitLab Enterprise Edition Premium][ee]. + +Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments) +and visualize your canary deployments right inside the Deploy Board, without +the need to leave GitLab. + +[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) + +### Kubernetes monitoring + +Automatically detect and monitor Kubernetes metrics. Automatic monitoring of +[NGINX ingress](./prometheus_library/nginx.md) is also supported. + +[> Read more about Kubernetes monitoring](prometheus_library/kubernetes.md) + +### Auto DevOps + +Auto DevOps automatically detects, builds, tests, deploys, and monitors your +applications. + +To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring) +you will need the Kubernetes project integration enabled. + +[> Read more about Auto DevOps](../../../topics/autodevops/index.md) + +### Web terminals + +NOTE: **Note:** +Introduced in GitLab 8.15. You must be the project owner or have `master` permissions +to use terminals. Support is limited to the first container in the first pod of your environment. When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals) @@ -70,3 +126,5 @@ Docker and Kubernetes, so you get a new shell session within your existing containers. To use this integration, you should deploy to Kubernetes using the deployment variables above, ensuring any pods you create are labelled with `app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! + +[ee]: https://about.gitlab.com/gitlab-ee/ diff --git a/doc/user/project/integrations/prometheus_library/cloudwatch.md b/doc/user/project/integrations/prometheus_library/cloudwatch.md index cc5cee36d28..34a0b97a171 100644 --- a/doc/user/project/integrations/prometheus_library/cloudwatch.md +++ b/doc/user/project/integrations/prometheus_library/cloudwatch.md @@ -1,8 +1,13 @@ # Monitoring AWS Resources + > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4 GitLab has support for automatically detecting and monitoring AWS resources, starting with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). This is provided by leveraging the official [Cloudwatch exporter](https://github.com/prometheus/cloudwatch_exporter), which translates [Cloudwatch metrics](https://aws.amazon.com/cloudwatch/) into a Prometheus readable form. +## Requirements + +The [Prometheus service](../prometheus/index.md) must be enabled. + ## Metrics supported | Name | Query | diff --git a/doc/user/project/integrations/prometheus_library/haproxy.md b/doc/user/project/integrations/prometheus_library/haproxy.md index d4b5911a91c..518018e5839 100644 --- a/doc/user/project/integrations/prometheus_library/haproxy.md +++ b/doc/user/project/integrations/prometheus_library/haproxy.md @@ -3,6 +3,10 @@ GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form. +## Requirements + +The [Prometheus service](../prometheus/index.md) must be enabled. + ## Metrics supported | Name | Query | diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index 4d39ae0c4fa..518683965e8 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -1,7 +1,13 @@ # Monitoring Kubernetes + > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935) in GitLab 9.0 -GitLab has support for automatically detecting and monitoring Kubernetes metrics. Kubernetes exposes Node level metrics out of the box via the built-in [Prometheus metrics support in cAdvisor](https://github.com/google/cadvisor). No additional services or exporters are needed. +GitLab has support for automatically detecting and monitoring Kubernetes metrics. + +## Requirements + +The [Prometheus](../prometheus.md) and [Kubernetes](../kubernetes.md) +integration services must be enabled. ## Metrics supported @@ -23,4 +29,4 @@ Prometheus server up and running. You have two options here: In order to isolate and only display relevant metrics for a given environment however, GitLab needs a method to detect which labels are associated. To do this, GitLab will [look for an `environment` label](metrics.md#identifying-environments). -If you are using [GitLab Auto-Deploy][../../../ci/autodeploy/index.md] and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. +If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md index bab22f9a384..7fb8369d3c1 100644 --- a/doc/user/project/integrations/prometheus_library/nginx.md +++ b/doc/user/project/integrations/prometheus_library/nginx.md @@ -1,8 +1,13 @@ # Monitoring NGINX + > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4 GitLab has support for automatically detecting and monitoring NGINX. This is provided by leveraging the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates [VTS statistics](https://github.com/vozlt/nginx-module-vts) into a Prometheus readable form. +## Requirements + +The [Prometheus service](../prometheus/index.md) must be enabled. + ## Metrics supported | Name | Query | diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index 17a47cfa646..e6f13d0630b 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -1,8 +1,13 @@ # Monitoring NGINX Ingress Controller + > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5 GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress/blob/master/controllers/nginx/Changelog.md#09-beta1) of the ingress. +## Requirements + +The [Prometheus service](../prometheus/index.md) must be enabled. + ## Metrics supported | Name | Query | diff --git a/doc/user/project/issues/img/button_close_issue.png b/doc/user/project/issues/img/button_close_issue.png Binary files differindex 8fb2e23f58a..8fb2e23f58a 100755..100644 --- a/doc/user/project/issues/img/button_close_issue.png +++ b/doc/user/project/issues/img/button_close_issue.png diff --git a/doc/user/project/issues/img/closing_and_related_issues.png b/doc/user/project/issues/img/closing_and_related_issues.png Binary files differindex c6543e85fdb..c6543e85fdb 100755..100644 --- a/doc/user/project/issues/img/closing_and_related_issues.png +++ b/doc/user/project/issues/img/closing_and_related_issues.png diff --git a/doc/user/project/issues/img/confidential_issues_create.png b/doc/user/project/issues/img/confidential_issues_create.png Binary files differindex 0a141eb39f8..0a141eb39f8 100755..100644 --- a/doc/user/project/issues/img/confidential_issues_create.png +++ b/doc/user/project/issues/img/confidential_issues_create.png diff --git a/doc/user/project/issues/img/confidential_issues_search_guest.png b/doc/user/project/issues/img/confidential_issues_search_guest.png Binary files differindex dc1b4ba8ad7..dc1b4ba8ad7 100755..100644 --- a/doc/user/project/issues/img/confidential_issues_search_guest.png +++ b/doc/user/project/issues/img/confidential_issues_search_guest.png diff --git a/doc/user/project/issues/img/confidential_issues_search_master.png b/doc/user/project/issues/img/confidential_issues_search_master.png Binary files differindex fc01f4da9db..fc01f4da9db 100755..100644 --- a/doc/user/project/issues/img/confidential_issues_search_master.png +++ b/doc/user/project/issues/img/confidential_issues_search_master.png diff --git a/doc/user/project/issues/img/due_dates_create.png b/doc/user/project/issues/img/due_dates_create.png Binary files differindex ece35d44213..ece35d44213 100755..100644 --- a/doc/user/project/issues/img/due_dates_create.png +++ b/doc/user/project/issues/img/due_dates_create.png diff --git a/doc/user/project/issues/img/due_dates_edit_sidebar.png b/doc/user/project/issues/img/due_dates_edit_sidebar.png Binary files differindex d1c7d1eb7e9..d1c7d1eb7e9 100755..100644 --- a/doc/user/project/issues/img/due_dates_edit_sidebar.png +++ b/doc/user/project/issues/img/due_dates_edit_sidebar.png diff --git a/doc/user/project/issues/img/due_dates_issues_index_page.png b/doc/user/project/issues/img/due_dates_issues_index_page.png Binary files differindex 94679436b32..94679436b32 100755..100644 --- a/doc/user/project/issues/img/due_dates_issues_index_page.png +++ b/doc/user/project/issues/img/due_dates_issues_index_page.png diff --git a/doc/user/project/issues/img/due_dates_todos.png b/doc/user/project/issues/img/due_dates_todos.png Binary files differindex 4c124c97f67..4c124c97f67 100755..100644 --- a/doc/user/project/issues/img/due_dates_todos.png +++ b/doc/user/project/issues/img/due_dates_todos.png diff --git a/doc/user/project/issues/img/issue_board.png b/doc/user/project/issues/img/issue_board.png Binary files differindex 1759b28a9ef..1759b28a9ef 100755..100644 --- a/doc/user/project/issues/img/issue_board.png +++ b/doc/user/project/issues/img/issue_board.png diff --git a/doc/user/project/issues/img/issue_template.png b/doc/user/project/issues/img/issue_template.png Binary files differindex c63229a4af2..c63229a4af2 100755..100644 --- a/doc/user/project/issues/img/issue_template.png +++ b/doc/user/project/issues/img/issue_template.png diff --git a/doc/user/project/issues/img/mention_in_issue.png b/doc/user/project/issues/img/mention_in_issue.png Binary files differindex c762a812138..c762a812138 100755..100644 --- a/doc/user/project/issues/img/mention_in_issue.png +++ b/doc/user/project/issues/img/mention_in_issue.png diff --git a/doc/user/project/issues/img/mention_in_merge_request.png b/doc/user/project/issues/img/mention_in_merge_request.png Binary files differindex 681e086d6e0..681e086d6e0 100755..100644 --- a/doc/user/project/issues/img/mention_in_merge_request.png +++ b/doc/user/project/issues/img/mention_in_merge_request.png diff --git a/doc/user/project/issues/img/merge_request_closes_issue.png b/doc/user/project/issues/img/merge_request_closes_issue.png Binary files differindex 6fd27738843..6fd27738843 100755..100644 --- a/doc/user/project/issues/img/merge_request_closes_issue.png +++ b/doc/user/project/issues/img/merge_request_closes_issue.png diff --git a/doc/user/project/issues/img/new_issue.png b/doc/user/project/issues/img/new_issue.png Binary files differindex e72ac49d6b9..e72ac49d6b9 100755..100644 --- a/doc/user/project/issues/img/new_issue.png +++ b/doc/user/project/issues/img/new_issue.png diff --git a/doc/user/project/issues/img/new_issue_from_issue_board.png b/doc/user/project/issues/img/new_issue_from_issue_board.png Binary files differindex 9c2b3ff50fa..9c2b3ff50fa 100755..100644 --- a/doc/user/project/issues/img/new_issue_from_issue_board.png +++ b/doc/user/project/issues/img/new_issue_from_issue_board.png diff --git a/doc/user/project/issues/img/new_issue_from_open_issue.png b/doc/user/project/issues/img/new_issue_from_open_issue.png Binary files differindex 2aed5372830..2aed5372830 100755..100644 --- a/doc/user/project/issues/img/new_issue_from_open_issue.png +++ b/doc/user/project/issues/img/new_issue_from_open_issue.png diff --git a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png Binary files differindex cddf36b7457..cddf36b7457 100755..100644 --- a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png +++ b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png diff --git a/doc/user/project/issues/img/new_issue_from_tracker_list.png b/doc/user/project/issues/img/new_issue_from_tracker_list.png Binary files differindex 7e5413f0b7d..7e5413f0b7d 100755..100644 --- a/doc/user/project/issues/img/new_issue_from_tracker_list.png +++ b/doc/user/project/issues/img/new_issue_from_tracker_list.png diff --git a/doc/user/project/issues/img/sidebar_confidential_issue.png b/doc/user/project/issues/img/sidebar_confidential_issue.png Binary files differindex d99a1ca756e..d99a1ca756e 100755..100644 --- a/doc/user/project/issues/img/sidebar_confidential_issue.png +++ b/doc/user/project/issues/img/sidebar_confidential_issue.png diff --git a/doc/user/project/issues/img/sidebar_not_confidential_issue.png b/doc/user/project/issues/img/sidebar_not_confidential_issue.png Binary files differindex 2e6cbbc5b3a..2e6cbbc5b3a 100755..100644 --- a/doc/user/project/issues/img/sidebar_not_confidential_issue.png +++ b/doc/user/project/issues/img/sidebar_not_confidential_issue.png diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 46fa4378fe7..453e10184f0 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding. If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a -[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). +[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com). The `namespace` is defined by your username on GitLab.com, or the group name you created this project under. @@ -73,6 +73,8 @@ Pages wildcard domain. This guide is valid for any GitLab instance, you just need to replace Pages wildcard domain on GitLab.com (`*.gitlab.io`) with your own. +Learn more about [namespaces](../../group/index.md#namespaces). + ### Practical examples #### Project Websites diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index 53fd1786cfa..0096f8507d2 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -1,9 +1,14 @@ +--- +last_updated: 2017-09-28 +--- + # GitLab Pages from A to Z: Part 3 -> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide || +> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles)**: user guide || > **Level**: beginner || > **Author**: [Marcia Ramos](https://gitlab.com/marcia) || -> **Publication date:** 2017/02/22 +> **Publication date:** 2017-02-22 || +> **Last updated**: 2017-09-28 - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) @@ -16,6 +21,21 @@ As described in the previous part of this series, setting up GitLab Pages with c These steps assume you've already [set your site up](getting_started_part_two.md) and and it's served under the default Pages domain `namespace.gitlab.io`, or `namespace.gitlab.io/project-name`. +### Adding your custom domain to GitLab Pages + +To use one or more custom domain with your Pages site, there are two things +you should consider first, which we'll cover in this guide: + +1. Either if you're adding a **root domain** or a **subdomain**, for which +you'll need to set up [DNS records](#dns-records) +1. Whether you want to add an [SSL/TLS certificate](#ssl-tls-certificates) or not + +To finish the association, you need to [add your domain to your project's Pages settings](#add-your-custom-domain-to-gitlab-pages-settings). + +Let's start from the beginning with [DNS records](#dns-records). +If you already know how they work and want to skip the introduction to DNS, +you may be interested in skipping it until the [TL;DR](#tl-dr) section below. + ### DNS Records A Domain Name System (DNS) web service routes visitors to websites @@ -99,6 +119,29 @@ domain. E.g., **do not** point your `subdomain.domain.com` to `namespace.gitlab.io.` or `namespace.gitlab.io/`. > - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`. +### Add your custom domain to GitLab Pages settings + +Once you've set the DNS record, you'll need navigate to your project's +**Setting > Pages** and click **+ New domain** to add your custom domain to +GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssl-tls-certificates) +to make your website accessible under HTTPS or leave it blank. If don't add a certificate, +your site will be accessible only via HTTP: + +![Add new domain](img/add_certificate_to_pages.png) + +You can add more than one alias (custom domains and subdomains) to the same project. +An alias can be understood as having many doors leading to the same room. + +All the aliases you've set to your site will be listed on **Setting > Pages**. +From that page, you can view, add, and remove them. + +Note that [DNS propagation may take some time (up to 24h)](http://www.inmotionhosting.com/support/domain-names/dns-nameserver-changes/domain-names-dns-changes), +although it's usually a matter of minutes to complete. Until it does, visit attempts +to your domain will respond with a 404. + +Read through the [general documentation on GitLab Pages](introduction.md#add-a-custom-domain-to-your-pages-website) to learn more about adding +custom domains to GitLab Pages sites. + ### SSL/TLS Certificates Every GitLab Pages project on GitLab.com will be available under diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index 9ecf7a3a8e7..4fcdfa7b281 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -28,7 +28,8 @@ In general there are two types of pages one might create: - Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) In GitLab, usernames and groupnames are unique and we often refer to them -as namespaces. There can be only one namespace in a GitLab instance. Below you +as [namespaces](../../group/index.md#namespaces). There can be only one namespace +in a GitLab instance. Below you can see the connection between the type of GitLab Pages, what the project name that is created on GitLab looks like and the website URL it will be ultimately be served on. @@ -98,6 +99,9 @@ The steps to create a project page for a user or a group are identical: A user's project will be served under `http(s)://username.example.io/projectname` whereas a group's project under `http(s)://groupname.example.io/projectname`. +For practical examples for group and project Pages, read through the guide +[GitLab Pages from A to Z: Part 1 - Static sites and GitLab Pages domains](getting_started_part_one.md#practical-examples). + ## Quick Start Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on @@ -111,6 +115,9 @@ The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that gives you absolute control over the build process. You can actually watch your website being built live by following the CI job traces. +For a simplified user guide on setting up GitLab CI/CD for Pages, read through +the article [GitLab Pages from A to Z: Part 4 - Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md#creating-and-tweaking-gitlab-ci-yml-for-gitlab-pages) + > **Note:** > Before reading this section, make sure you familiarize yourself with GitLab CI > and the specific syntax of[`.gitlab-ci.yml`][yaml] by @@ -311,6 +318,9 @@ Visit the GitLab Pages group for a full list of example projects: ### Add a custom domain to your Pages website +For a complete guide on Pages domains, read through the article +[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates) + If this setting is enabled by your GitLab administrator, you should be able to see the **New Domain** button when visiting your project's settings through the gear icon in the top right and then navigating to **Pages**. @@ -349,6 +359,9 @@ private key when adding a new domain. ![Pages upload cert](img/pages_upload_cert.png) +For a complete guide on Pages domains, read through the article +[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates) + ### Custom error codes pages You can provide your own 403 and 404 error pages by creating the `403.html` and @@ -387,6 +400,8 @@ If you are using GitLab.com to host your website, then: The rest of the guide still applies. +See also: [GitLab Pages from A to Z: Part 1 - Static sites and GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain). + ## Limitations When using Pages under the general domain of a GitLab instance (`*.example.io`), diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md index dfe43c6b691..29e04a0ccf0 100644 --- a/doc/user/project/repository/gpg_signed_commits/index.md +++ b/doc/user/project/repository/gpg_signed_commits/index.md @@ -113,25 +113,25 @@ started: 1. Use the following command to list the private GPG key you just created: ``` - gpg --list-secret-keys --keyid-format 0xLONG mr@robot.sh + gpg --list-secret-keys --keyid-format LONG mr@robot.sh ``` Replace `mr@robot.sh` with the email address you entered above. 1. Copy the GPG key ID that starts with `sec`. In the following example, that's - `0x30F2B65B9246B6CA`: + `30F2B65B9246B6CA`: ``` - sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC] + sec rsa4096/30F2B65B9246B6CA 2017-08-18 [SC] D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA uid [ultimate] Mr. Robot <mr@robot.sh> - ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E] + ssb rsa4096/B7ABC0813E4028C0 2017-08-18 [E] ``` 1. Export the public key of that ID (replace your key ID from the previous step): ``` - gpg --armor --export 0x30F2B65B9246B6CA + gpg --armor --export 30F2B65B9246B6CA ``` 1. Finally, copy the public key and [add it in your profile settings](#adding-a-gpg-key-to-your-account) @@ -167,28 +167,28 @@ key to use. 1. Use the following command to list the private GPG key you just created: ``` - gpg --list-secret-keys --keyid-format 0xLONG mr@robot.sh + gpg --list-secret-keys --keyid-format LONG mr@robot.sh ``` Replace `mr@robot.sh` with the email address you entered above. 1. Copy the GPG key ID that starts with `sec`. In the following example, that's - `0x30F2B65B9246B6CA`: + `30F2B65B9246B6CA`: ``` - sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC] + sec rsa4096/30F2B65B9246B6CA 2017-08-18 [SC] D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA uid [ultimate] Mr. Robot <mr@robot.sh> - ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E] + ssb rsa4096/B7ABC0813E4028C0 2017-08-18 [E] ``` 1. Tell Git to use that key to sign the commits: ``` - git config --global user.signingkey 0x30F2B65B9246B6CA + git config --global user.signingkey 30F2B65B9246B6CA ``` - Replace `0x30F2B65B9246B6CA` with your GPG key ID. + Replace `30F2B65B9246B6CA` with your GPG key ID. ## Signing commits diff --git a/doc/user/project/repository/img/contributors_graph.png b/doc/user/project/repository/img/contributors_graph.png Binary files differindex c31da7aa1ff..c31da7aa1ff 100755..100644 --- a/doc/user/project/repository/img/contributors_graph.png +++ b/doc/user/project/repository/img/contributors_graph.png diff --git a/doc/user/project/repository/img/repo_graph.png b/doc/user/project/repository/img/repo_graph.png Binary files differindex 28da8ad9589..28da8ad9589 100755..100644 --- a/doc/user/project/repository/img/repo_graph.png +++ b/doc/user/project/repository/img/repo_graph.png diff --git a/doc/user/project/settings/img/general_settings.png b/doc/user/project/settings/img/general_settings.png Binary files differindex 96f5b84871f..96f5b84871f 100755..100644 --- a/doc/user/project/settings/img/general_settings.png +++ b/doc/user/project/settings/img/general_settings.png diff --git a/doc/user/project/settings/img/merge_requests_settings.png b/doc/user/project/settings/img/merge_requests_settings.png Binary files differindex b1f2dfa7376..b1f2dfa7376 100755..100644 --- a/doc/user/project/settings/img/merge_requests_settings.png +++ b/doc/user/project/settings/img/merge_requests_settings.png diff --git a/doc/user/project/settings/img/sharing_and_permissions_settings.png b/doc/user/project/settings/img/sharing_and_permissions_settings.png Binary files differindex 7767a3d7187..0f9cf9512af 100755..100644 --- a/doc/user/project/settings/img/sharing_and_permissions_settings.png +++ b/doc/user/project/settings/img/sharing_and_permissions_settings.png diff --git a/doc/user/search/img/issues_any_assignee.png b/doc/user/search/img/issues_any_assignee.png Binary files differindex 2f902bcc66c..2f902bcc66c 100755..100644 --- a/doc/user/search/img/issues_any_assignee.png +++ b/doc/user/search/img/issues_any_assignee.png diff --git a/doc/user/search/img/issues_assigned_to_you.png b/doc/user/search/img/issues_assigned_to_you.png Binary files differindex 36c670eedd5..36c670eedd5 100755..100644 --- a/doc/user/search/img/issues_assigned_to_you.png +++ b/doc/user/search/img/issues_assigned_to_you.png diff --git a/doc/user/search/img/issues_author.png b/doc/user/search/img/issues_author.png Binary files differindex 792f9746db6..792f9746db6 100755..100644 --- a/doc/user/search/img/issues_author.png +++ b/doc/user/search/img/issues_author.png diff --git a/doc/user/search/img/issues_mrs_shortcut.png b/doc/user/search/img/issues_mrs_shortcut.png Binary files differindex 6380b337b54..6380b337b54 100755..100644 --- a/doc/user/search/img/issues_mrs_shortcut.png +++ b/doc/user/search/img/issues_mrs_shortcut.png diff --git a/doc/user/search/img/left_menu_bar.png b/doc/user/search/img/left_menu_bar.png Binary files differindex d68a71cba8e..d68a71cba8e 100755..100644 --- a/doc/user/search/img/left_menu_bar.png +++ b/doc/user/search/img/left_menu_bar.png diff --git a/doc/user/search/img/project_search.png b/doc/user/search/img/project_search.png Binary files differindex 3150b40de29..3150b40de29 100755..100644 --- a/doc/user/search/img/project_search.png +++ b/doc/user/search/img/project_search.png diff --git a/doc/user/search/img/search_issues_board.png b/doc/user/search/img/search_issues_board.png Binary files differindex 84048ae6a02..84048ae6a02 100755..100644 --- a/doc/user/search/img/search_issues_board.png +++ b/doc/user/search/img/search_issues_board.png diff --git a/doc/user/search/img/sort_projects.png b/doc/user/search/img/sort_projects.png Binary files differindex 9bf2770b299..9bf2770b299 100755..100644 --- a/doc/user/search/img/sort_projects.png +++ b/doc/user/search/img/sort_projects.png diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 4f905674d8c..d6cfa524a3a 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -51,36 +51,34 @@ Feature: Project Issues @javascript Scenario: Visiting Issues after being sorted the list Given I visit project "Shop" issues page - And I sort the list by "Oldest updated" + And I sort the list by "Last updated" And I visit my project's home page And I visit project "Shop" issues page - Then The list should be sorted by "Oldest updated" + Then The list should be sorted by "Last updated" @javascript Scenario: Visiting Merge Requests after being sorted the list Given project "Shop" has a "Bugfix MR" merge request open And I visit project "Shop" issues page - And I sort the list by "Oldest updated" + And I sort the list by "Last updated" And I visit project "Shop" merge requests page - Then The list should be sorted by "Oldest updated" + Then The list should be sorted by "Last updated" @javascript Scenario: Visiting Merge Requests from a differente Project after sorting Given project "Shop" has a "Bugfix MR" merge request open And I visit project "Shop" merge requests page - And I sort the list by "Oldest updated" + And I sort the list by "Last updated" And I visit dashboard merge requests page - Then The list should be sorted by "Oldest updated" + Then The list should be sorted by "Last updated" @javascript Scenario: Sort issues by upvotes/downvotes Given project "Shop" have "Bugfix" open issue And issue "Release 0.4" have 2 upvotes and 1 downvote And issue "Tweet control" have 1 upvote and 2 downvotes - And I sort the list by "Most popular" - Then The list should be sorted by "Most popular" - And I sort the list by "Least popular" - Then The list should be sorted by "Least popular" + And I sort the list by "Popularity" + Then The list should be sorted by "Popularity" # Markdown diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 0ebeded7fc5..349fa2663a7 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -91,28 +91,26 @@ Feature: Project Merge Requests @javascript Scenario: Visiting Merge Requests after being sorted the list Given I visit project "Shop" merge requests page - And I sort the list by "Oldest updated" + And I sort the list by "Last updated" And I visit my project's home page And I visit project "Shop" merge requests page - Then The list should be sorted by "Oldest updated" + Then The list should be sorted by "Last updated" @javascript Scenario: Visiting Merge Requests from a different Project after sorting Given I visit project "Shop" merge requests page - And I sort the list by "Oldest updated" + And I sort the list by "Last updated" And I visit dashboard merge requests page - Then The list should be sorted by "Oldest updated" + Then The list should be sorted by "Last updated" @javascript - Scenario: Sort merge requests by upvotes/downvotes + Scenario: Sort merge requests by upvotes Given project "Shop" have "Bug NS-05" open merge request with diffs inside And project "Shop" have "Bug NS-06" open merge request And merge request "Bug NS-04" have 2 upvotes and 1 downvote And merge request "Bug NS-06" have 1 upvote and 2 downvotes - And I sort the list by "Most popular" - Then The list should be sorted by "Most popular" - And I sort the list by "Least popular" - Then The list should be sorted by "Least popular" + And I sort the list by "Popularity" + Then The list should be sorted by "Popularity" @javascript Scenario: I comment on a merge request diff diff --git a/features/project/service.feature b/features/project/service.feature deleted file mode 100644 index 54f07ebca92..00000000000 --- a/features/project/service.feature +++ /dev/null @@ -1,87 +0,0 @@ -Feature: Project Services - Background: - Given I sign in as a user - And I own project "Shop" - - Scenario: I should see project services - When I visit project "Shop" services page - Then I should see list of available services - - Scenario: Activate hipchat service - When I visit project "Shop" services page - And I click hipchat service link - And I fill hipchat settings - Then I should see the Hipchat success message - - Scenario: Activate hipchat service with custom server - When I visit project "Shop" services page - And I click hipchat service link - And I fill hipchat settings with custom server - Then I should see the Hipchat success message - - Scenario: Activate pivotaltracker service - When I visit project "Shop" services page - And I click pivotaltracker service link - And I fill pivotaltracker settings - Then I should see the Pivotaltracker success message - - Scenario: Activate Flowdock service - When I visit project "Shop" services page - And I click Flowdock service link - And I fill Flowdock settings - Then I should see the Flowdock success message - - Scenario: Activate Assembla service - When I visit project "Shop" services page - And I click Assembla service link - And I fill Assembla settings - Then I should see the Assembla success message - - Scenario: Activate Slack notifications service - When I visit project "Shop" services page - And I click Slack notifications service link - And I fill Slack notifications settings - Then I should see the Slack notifications success message - - Scenario: Activate Pushover service - When I visit project "Shop" services page - And I click Pushover service link - And I fill Pushover settings - Then I should see the Pushover success message - - Scenario: Activate email on push service - When I visit project "Shop" services page - And I click email on push service link - And I fill email on push settings - Then I should see the Emails on push success message - - Scenario: Activate JIRA service - When I visit project "Shop" services page - And I click jira service link - And I fill jira settings - Then I should see the JIRA success message - - Scenario: Activate Irker (IRC Gateway) service - When I visit project "Shop" services page - And I click Irker service link - And I fill Irker settings - Then I should see the Irker success message - - Scenario: Activate Atlassian Bamboo CI service - When I visit project "Shop" services page - And I click Atlassian Bamboo CI service link - And I fill Atlassian Bamboo CI settings - Then I should see the Bamboo success message - And I should see empty field Change Password - - Scenario: Activate jetBrains TeamCity CI service - When I visit project "Shop" services page - And I click jetBrains TeamCity CI service link - And I fill jetBrains TeamCity CI settings - Then I should see the JetBrains success message - - Scenario: Activate Asana service - When I visit project "Shop" services page - And I click Asana service link - And I fill Asana settings - Then I should see the Asana success message diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature deleted file mode 100644 index cbbea237825..00000000000 --- a/features/project/shortcuts.feature +++ /dev/null @@ -1,63 +0,0 @@ -@dashboard -Feature: Project Shortcuts - Background: - Given I sign in as a user - And I own a project - And I visit my project's commits page - - @javascript - Scenario: Navigate to files tab - Given I press "g" and "f" - Then the active main tab should be Repository - Then the active sub tab should be Files - - @javascript - Scenario: Navigate to commits tab - Given I visit my project's files page - Given I press "g" and "c" - Then the active main tab should be Repository - Then the active sub tab should be Commits - - @javascript - Scenario: Navigate to graph tab - Given I press "g" and "n" - Then the active sub tab should be Graph - And the active main tab should be Repository - - @javascript - Scenario: Navigate to repository charts tab - Given I press "g" and "d" - Then the active sub tab should be Charts - And the active main tab should be Repository - - @javascript - Scenario: Navigate to issues tab - Given I press "g" and "i" - Then the active main tab should be Issues - - @javascript - Scenario: Navigate to merge requests tab - Given I press "g" and "m" - Then the active main tab should be Merge Requests - - @javascript - Scenario: Navigate to snippets tab - Given I press "g" and "s" - Then the active main tab should be Snippets - - @javascript - Scenario: Navigate to wiki tab - Given I press "g" and "w" - Then the active main tab should be Wiki - - @javascript - Scenario: Navigate to project home - Given I press "g" and "p" - Then the active sub tab should be Home - And the active main tab should be Project - - @javascript - Scenario: Navigate to project feed - Given I press "g" and "e" - Then the active sub tab should be Activity - And the active main tab should be Project diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 305fff37c41..318e054e978 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -139,7 +139,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'The diff links to both the previous and current image' do - links = page.all('.two-up span div a') + links = page.all('.file-actions a') expect(links[0]['href']).to match %r{blob/#{sample_image_commit.old_blob_id}} expect(links[1]['href']).to match %r{blob/#{sample_image_commit.new_blob_id}} end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index b9460f5b534..2c3ef2efd52 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -223,7 +223,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end end - step 'The list should be sorted by "Most popular"' do + step 'The list should be sorted by "Popularity"' do page.within '.issues-list' do page.within 'li.issue:nth-child(1)' do expect(page).to have_content 'Release 0.4' diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 0d49a4ab90d..dde918e3d41 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -222,7 +222,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end - step 'The list should be sorted by "Most popular"' do + step 'The list should be sorted by "Popularity"' do page.within '.mr-list' do page.within 'li.merge-request:nth-child(1)' do expect(page).to have_content 'Bug NS-04' diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb deleted file mode 100644 index cebf09750b0..00000000000 --- a/features/steps/project/project_shortcuts.rb +++ /dev/null @@ -1,42 +0,0 @@ -class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedProjectTab - include SharedShortcuts - - step 'I press "g" and "f"' do - find('body').native.send_key('g') - find('body').native.send_key('f') - end - - step 'I press "g" and "c"' do - find('body').native.send_key('g') - find('body').native.send_key('c') - end - - step 'I press "g" and "n"' do - find('body').native.send_key('g') - find('body').native.send_key('n') - end - - step 'I press "g" and "d"' do - find('body').native.send_key('g') - find('body').native.send_key('d') - end - - step 'I press "g" and "s"' do - find('body').native.send_key('g') - find('body').native.send_key('s') - end - - step 'I press "g" and "w"' do - find('body').native.send_key('g') - find('body').native.send_key('w') - end - - step 'I press "g" and "e"' do - find('body').native.send_key('g') - find('body').native.send_key('e') - end -end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb deleted file mode 100644 index 7e2a357f6b2..00000000000 --- a/features/steps/project/services.rb +++ /dev/null @@ -1,224 +0,0 @@ -class Spinach::Features::ProjectServices < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I visit project "Shop" services page' do - visit project_settings_integrations_path(@project) - end - - step 'I should see list of available services' do - expect(page).to have_content 'Project services' - expect(page).to have_content 'Campfire' - expect(page).to have_content 'HipChat' - expect(page).to have_content 'Assembla' - expect(page).to have_content 'Pushover' - expect(page).to have_content 'Atlassian Bamboo' - expect(page).to have_content 'JetBrains TeamCity' - expect(page).to have_content 'Asana' - expect(page).to have_content 'Irker (IRC gateway)' - end - - step 'I should see service settings saved' do - expect(find_field('Active').value).to eq '1' - end - - step 'I click hipchat service link' do - click_link 'HipChat' - end - - step 'I fill hipchat settings' do - check 'Active' - fill_in 'Room', with: 'gitlab' - fill_in 'Token', with: 'verySecret' - click_button 'Save' - end - - step 'I should see the Hipchat success message' do - expect(page).to have_content 'HipChat activated.' - end - - step 'I fill hipchat settings with custom server' do - check 'Active' - fill_in 'Room', with: 'gitlab_custom' - fill_in 'Token', with: 'secretCustom' - fill_in 'Server', with: 'https://chat.example.com' - click_button 'Save' - end - - step 'I click pivotaltracker service link' do - click_link 'PivotalTracker' - end - - step 'I fill pivotaltracker settings' do - check 'Active' - fill_in 'Token', with: 'verySecret' - click_button 'Save' - end - - step 'I should see the Pivotaltracker success message' do - expect(page).to have_content 'PivotalTracker activated.' - end - - step 'I click Flowdock service link' do - click_link 'Flowdock' - end - - step 'I fill Flowdock settings' do - check 'Active' - fill_in 'Token', with: 'verySecret' - click_button 'Save' - end - - step 'I should see the Flowdock success message' do - expect(page).to have_content 'Flowdock activated.' - end - - step 'I click Assembla service link' do - click_link 'Assembla' - end - - step 'I fill Assembla settings' do - check 'Active' - fill_in 'Token', with: 'verySecret' - click_button 'Save' - end - - step 'I should see the Assembla success message' do - expect(page).to have_content 'Assembla activated.' - end - - step 'I click Asana service link' do - click_link 'Asana' - end - - step 'I fill Asana settings' do - check 'Active' - fill_in 'Api key', with: 'verySecret' - fill_in 'Restrict to branch', with: 'master' - click_button 'Save' - end - - step 'I should see the Asana success message' do - expect(page).to have_content 'Asana activated.' - end - - step 'I click email on push service link' do - click_link 'Emails on push' - end - - step 'I fill email on push settings' do - check 'Active' - fill_in 'Recipients', with: 'qa@company.name' - click_button 'Save' - end - - step 'I should see the Emails on push success message' do - expect(page).to have_content 'Emails on push activated.' - end - - step 'I click Irker service link' do - click_link 'Irker (IRC gateway)' - end - - step 'I fill Irker settings' do - check 'Active' - fill_in 'Recipients', with: 'irc://chat.freenode.net/#commits' - check 'Colorize messages' - click_button 'Save' - end - - step 'I should see the Irker success message' do - expect(page).to have_content 'Irker (IRC gateway) activated.' - end - - step 'I click Slack notifications service link' do - click_link 'Slack notifications' - end - - step 'I fill Slack notifications settings' do - check 'Active' - fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' - click_button 'Save' - end - - step 'I should see the Slack notifications success message' do - expect(page).to have_content 'Slack notifications activated.' - end - - step 'I click Pushover service link' do - click_link 'Pushover' - end - - step 'I fill Pushover settings' do - check 'Active' - fill_in 'Api key', with: 'verySecret' - fill_in 'User key', with: 'verySecret' - fill_in 'Device', with: 'myDevice' - select 'High Priority', from: 'Priority' - select 'Bike', from: 'Sound' - click_button 'Save' - end - - step 'I should see the Pushover success message' do - expect(page).to have_content 'Pushover activated.' - end - - step 'I click jira service link' do - click_link 'JIRA' - end - - step 'I fill jira settings' do - check 'Active' - - fill_in 'Web URL', with: 'http://jira.example' - fill_in 'JIRA API URL', with: 'http://jira.example/api' - fill_in 'Username', with: 'gitlab' - fill_in 'Password', with: 'gitlab' - click_button 'Save' - end - - step 'I should see the JIRA success message' do - expect(page).to have_content 'JIRA activated.' - end - - step 'I click Atlassian Bamboo CI service link' do - click_link 'Atlassian Bamboo CI' - end - - step 'I fill Atlassian Bamboo CI settings' do - check 'Active' - fill_in 'Bamboo url', with: 'http://bamboo.example.com' - fill_in 'Build key', with: 'KEY' - fill_in 'Username', with: 'user' - fill_in 'Password', with: 'verySecret' - click_button 'Save' - end - - step 'I should see the Bamboo success message' do - expect(page).to have_content 'Atlassian Bamboo CI activated.' - end - - step 'I should see empty field Change Password' do - click_link 'Atlassian Bamboo CI' - - expect(find_field('Enter new password').value).to be_nil - end - - step 'I click JetBrains TeamCity CI service link' do - click_link 'JetBrains TeamCity CI' - end - - step 'I fill JetBrains TeamCity CI settings' do - check 'Active' - fill_in 'Teamcity url', with: 'http://teamcity.example.com' - fill_in 'Build type', with: 'GitlabTest_Build' - fill_in 'Username', with: 'user' - fill_in 'Password', with: 'verySecret' - click_button 'Save' - end - - step 'I should see the JetBrains success message' do - expect(page).to have_content 'JetBrains TeamCity CI activated.' - end -end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 7c842ba88fb..714985f2051 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -109,10 +109,10 @@ module SharedIssuable edit_issuable end - step 'I sort the list by "Oldest updated"' do + step 'I sort the list by "Last updated"' do find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do - click_link "Oldest updated" + click_link "Last updated" end end @@ -124,16 +124,16 @@ module SharedIssuable end end - step 'I sort the list by "Most popular"' do + step 'I sort the list by "Popularity"' do find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do - click_link 'Most popular' + click_link 'Popularity' end end - step 'The list should be sorted by "Oldest updated"' do - expect(find('.issues-filters')).to have_content('Oldest updated') + step 'The list should be sorted by "Last updated"' do + expect(find('.issues-filters')).to have_content('Last updated') end step 'I click link "Next" in the sidebar' do diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c4c0fdda665..e79f988f549 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -75,7 +75,7 @@ module API raise RevokedError when AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) + User.find(access_token.resource_owner_id) end end @@ -84,11 +84,13 @@ module API return nil unless token_string.present? - find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes) - end + user = + find_user_by_authentication_token(token_string) || + find_user_by_personal_access_token(token_string, scopes) + + raise UnauthorizedError unless user - def current_user - @current_user + user end private @@ -107,7 +109,16 @@ module API end def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + return @access_token if defined?(@access_token) + + token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) + return @access_token = nil unless token + + @access_token = Doorkeeper::AccessToken.by_token(token) + raise UnauthorizedError unless @access_token + + @access_token.revoke_previous_refresh_token! + @access_token end def doorkeeper_request @@ -169,6 +180,7 @@ module API TokenNotFoundError = Class.new(StandardError) ExpiredError = Class.new(StandardError) RevokedError = Class.new(StandardError) + UnauthorizedError = Class.new(StandardError) class InsufficientScopeError < StandardError attr_reader :scopes diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 642c1140fcc..643c8e6fb8e 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -21,7 +21,10 @@ module API get ':id/repository/branches' do branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) - present paginate(branches), with: Entities::RepoBranch, project: user_project + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442 + Gitlab::GitalyClient.allow_n_plus_1_calls do + present paginate(branches), with: Entities::RepoBranch, project: user_project + end end resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do diff --git a/lib/api/custom_attributes_endpoints.rb b/lib/api/custom_attributes_endpoints.rb new file mode 100644 index 00000000000..5000aa0d9ac --- /dev/null +++ b/lib/api/custom_attributes_endpoints.rb @@ -0,0 +1,77 @@ +module API + module CustomAttributesEndpoints + extend ActiveSupport::Concern + + included do + attributable_class = name.demodulize.singularize + attributable_key = attributable_class.underscore + attributable_name = attributable_class.humanize(capitalize: false) + attributable_finder = "find_#{attributable_key}" + + helpers do + params :custom_attributes_key do + requires :key, type: String, desc: 'The key of the custom attribute' + end + end + + desc "Get all custom attributes on a #{attributable_name}" do + success Entities::CustomAttribute + end + get ':id/custom_attributes' do + resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend + authorize! :read_custom_attribute + + present resource.custom_attributes, with: Entities::CustomAttribute + end + + desc "Get a custom attribute on a #{attributable_name}" do + success Entities::CustomAttribute + end + params do + use :custom_attributes_key + end + get ':id/custom_attributes/:key' do + resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend + authorize! :read_custom_attribute + + custom_attribute = resource.custom_attributes.find_by!(key: params[:key]) + + present custom_attribute, with: Entities::CustomAttribute + end + + desc "Set a custom attribute on a #{attributable_name}" + params do + use :custom_attributes_key + requires :value, type: String, desc: 'The value of the custom attribute' + end + put ':id/custom_attributes/:key' do + resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend + authorize! :update_custom_attribute + + custom_attribute = resource.custom_attributes + .find_or_initialize_by(key: params[:key]) + + custom_attribute.update(value: params[:value]) + + if custom_attribute.valid? + present custom_attribute, with: Entities::CustomAttribute + else + render_validation_error!(custom_attribute) + end + end + + desc "Delete a custom attribute on a #{attributable_name}" + params do + use :custom_attributes_key + end + delete ':id/custom_attributes/:key' do + resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend + authorize! :update_custom_attribute + + resource.custom_attributes.find_by!(key: params[:key]).destroy + + status 204 + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 30b115b1b56..5d45b14f592 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -89,6 +89,9 @@ module API expose :ssh_url_to_repo, :http_url_to_repo, :web_url expose :name, :name_with_namespace expose :path, :path_with_namespace + expose :avatar_url do |project, options| + project.avatar_url(only_path: false) + end expose :star_count, :forks_count expose :created_at, :last_activity_at end @@ -146,9 +149,7 @@ module API expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? } expose :import_status expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :avatar_url do |user, options| - user.avatar_url(only_path: false) - end + expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds, as: :public_jobs @@ -193,8 +194,8 @@ module API class Group < Grape::Entity expose :id, :name, :path, :description, :visibility expose :lfs_enabled?, as: :lfs_enabled - expose :avatar_url do |user, options| - user.avatar_url(only_path: false) + expose :avatar_url do |group, options| + group.avatar_url(only_path: false) end expose :web_url expose :request_access_enabled @@ -234,6 +235,7 @@ module API class RepoCommitDetail < RepoCommit expose :stats, using: Entities::RepoCommitStats expose :status + expose :last_pipeline, using: 'API::Entities::PipelineBasic' end class RepoBranch < Grape::Entity @@ -244,7 +246,10 @@ module API end expose :merged do |repo_branch, options| - options[:project].repository.merged_to_root_ref?(repo_branch.name) + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442 + Gitlab::GitalyClient.allow_n_plus_1_calls do + options[:project].repository.merged_to_root_ref?(repo_branch.name) + end end expose :protected do |repo_branch, options| @@ -1031,5 +1036,10 @@ module API expose :failing_on_hosts expose :total_failures end + + class CustomAttribute < Grape::Entity + expose :key + expose :value + end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 00dbc2aee7a..1e8475ba3ec 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -3,6 +3,8 @@ module API include Gitlab::Utils include Helpers::Pagination + UnauthorizedError = Class.new(StandardError) + SUDO_HEADER = "HTTP_SUDO".freeze SUDO_PARAM = :sudo @@ -139,7 +141,7 @@ module API end def authenticate! - unauthorized! unless current_user && can?(initial_current_user, :access_api) + unauthorized! unless current_user end def authenticate_non_get! @@ -397,19 +399,27 @@ module API def initial_current_user return @initial_current_user if defined?(@initial_current_user) - Gitlab::Auth::UniqueIpsLimiter.limit_user! do - @initial_current_user ||= find_user_by_private_token(scopes: scopes_registered_for_endpoint) - @initial_current_user ||= doorkeeper_guard(scopes: scopes_registered_for_endpoint) - @initial_current_user ||= find_user_from_warden - - unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? - @initial_current_user = nil - end - @initial_current_user + begin + @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user } + rescue APIGuard::UnauthorizedError, UnauthorizedError + unauthorized! end end + def find_current_user + user = + find_user_by_private_token(scopes: scopes_registered_for_endpoint) || + doorkeeper_guard(scopes: scopes_registered_for_endpoint) || + find_user_from_warden + + return nil unless user + + raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) + + user + end + def sudo! return unless sudo_identifier return unless initial_current_user diff --git a/lib/api/internal.rb b/lib/api/internal.rb index c0fef56378f..a0557a609ca 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -136,7 +136,7 @@ module API codes = nil - ::Users::UpdateService.new(user).execute! do |user| + ::Users::UpdateService.new(current_user, user: user).execute! do |user| codes = user.generate_otp_backup_codes! end diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index bcc0833aa5c..0266bf2f717 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -35,7 +35,7 @@ module API new_notification_email = params.delete(:notification_email) if new_notification_email - ::Users::UpdateService.new(current_user, notification_email: new_notification_email).execute + ::Users::UpdateService.new(current_user, user: current_user, notification_email: new_notification_email).execute end notification_setting.update(declared_params(include_missing: false)) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 7dc19788462..aab7a6c3f93 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -70,8 +70,11 @@ module API optional :import_url, type: String, desc: 'URL from which the project is imported' end - def present_projects(options = {}) - projects = ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute + def load_projects + ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute + end + + def present_projects(projects, options = {}) projects = reorder_projects(projects) projects = projects.with_statistics if params[:statistics] projects = projects.with_issues_enabled if params[:with_issues_enabled] @@ -111,7 +114,7 @@ module API params[:user] = user - present_projects + present_projects load_projects end end @@ -124,7 +127,7 @@ module API use :statistics_params end get do - present_projects + present_projects load_projects end desc 'Create new project' do @@ -229,6 +232,18 @@ module API end end + desc 'List forks of this project' do + success Entities::Project + end + params do + use :collection_params + end + get ':id/forks' do + forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute + + present_projects forks + end + desc 'Update an existing project' do success Entities::Project end diff --git a/lib/api/users.rb b/lib/api/users.rb index bdebda58d3f..d07dc302717 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -6,12 +6,14 @@ module API allow_access_with_scope :read_user, if: -> (request) { request.get? } resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do + include CustomAttributesEndpoints + before do authenticate_non_get! end helpers do - def find_user(params) + def find_user_by_id(params) id = params[:user_id] || params[:id] User.find_by(id: id) || not_found!('User') end @@ -166,7 +168,7 @@ module API user_params[:password_expires_at] = Time.now if user_params[:password].present? - result = ::Users::UpdateService.new(user, user_params.except(:extern_uid, :provider)).execute + result = ::Users::UpdateService.new(current_user, user_params.except(:extern_uid, :provider).merge(user: user)).execute if result[:status] == :success present user, with: Entities::UserPublic @@ -326,7 +328,7 @@ module API user = User.find_by(id: params.delete(:id)) not_found!('User') unless user - email = Emails::CreateService.new(user, declared_params(include_missing: false)).execute + email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute if email.errors.blank? NotificationService.new.new_email(email) @@ -367,7 +369,7 @@ module API not_found!('Email') unless email destroy_conditionally!(email) do |email| - Emails::DestroyService.new(current_user, email: email.email).execute + Emails::DestroyService.new(current_user, user: user, email: email.email).execute end user.update_secondary_emails! @@ -430,7 +432,7 @@ module API resource :impersonation_tokens do helpers do def finder(options = {}) - user = find_user(params) + user = find_user_by_id(params) PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) end @@ -672,7 +674,7 @@ module API requires :email, type: String, desc: 'The new email' end post "emails" do - email = Emails::CreateService.new(current_user, declared_params).execute + email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute if email.errors.blank? NotificationService.new.new_email(email) @@ -691,7 +693,7 @@ module API not_found!('Email') unless email destroy_conditionally!(email) do |email| - Emails::DestroyService.new(current_user, email: email.email).execute + Emails::DestroyService.new(current_user, user: current_user, email: email.email).execute end current_user.update_secondary_emails! diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index b9a573d3542..3cf3939994a 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -79,7 +79,7 @@ module Backup # - 1495527122_gitlab_backup.tar # - 1495527068_2017_05_23_gitlab_backup.tar # - 1495527097_2017_05_23_9.3.0-pre_gitlab_backup.tar - next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+.*)?)?_gitlab_backup\.tar$/ + next unless file =~ /^(\d{10})(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+((-|\.)(pre|rc\d))?(-ee)?)?)?_gitlab_backup\.tar$/ timestamp = $1.to_i diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 9923ec4e870..88b17e12576 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -45,8 +45,9 @@ module Banzai whitelist[:elements].push('abbr') whitelist[:attributes]['abbr'] = %w(title) - # Disallow `name` attribute globally + # Disallow `name` attribute globally, allow on `a` whitelist[:attributes][:all].delete('name') + whitelist[:attributes]['a'].push('name') # Allow any protocol in `a` elements... whitelist[:protocols].delete('a') diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index a65bbe23958..e0a8ca653cb 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -34,7 +34,8 @@ module Banzai { namespace: :owner }, { group: [:owners, :group_members] }, :invited_groups, - :project_members + :project_members, + :project_feature ] } ), diff --git a/lib/github/client.rb b/lib/github/client.rb index 9c476df7d46..29bd9c1f39e 100644 --- a/lib/github/client.rb +++ b/lib/github/client.rb @@ -1,6 +1,7 @@ module Github class Client TIMEOUT = 60 + DEFAULT_PER_PAGE = 100 attr_reader :connection, :rate_limit @@ -20,7 +21,7 @@ module Github exceed, reset_in = rate_limit.get sleep reset_in if exceed - Github::Response.new(connection.get(url, query)) + Github::Response.new(connection.get(url, { per_page: DEFAULT_PER_PAGE }.merge(query))) end private diff --git a/lib/github/import.rb b/lib/github/import.rb index 9354e142d3d..c0cd8382875 100644 --- a/lib/github/import.rb +++ b/lib/github/import.rb @@ -1,46 +1,13 @@ require_relative 'error' +require_relative 'import/issue' +require_relative 'import/legacy_diff_note' +require_relative 'import/merge_request' +require_relative 'import/note' module Github class Import include Gitlab::ShellAdapter - class MergeRequest < ::MergeRequest - self.table_name = 'merge_requests' - - self.reset_callbacks :create - self.reset_callbacks :save - self.reset_callbacks :commit - self.reset_callbacks :update - self.reset_callbacks :validate - end - - class Issue < ::Issue - self.table_name = 'issues' - - self.reset_callbacks :save - self.reset_callbacks :create - self.reset_callbacks :commit - self.reset_callbacks :update - self.reset_callbacks :validate - end - - class Note < ::Note - self.table_name = 'notes' - - self.reset_callbacks :save - self.reset_callbacks :commit - self.reset_callbacks :update - self.reset_callbacks :validate - end - - class LegacyDiffNote < ::LegacyDiffNote - self.table_name = 'notes' - - self.reset_callbacks :commit - self.reset_callbacks :update - self.reset_callbacks :validate - end - attr_reader :project, :repository, :repo, :repo_url, :wiki_url, :options, :errors, :cached, :verbose @@ -202,13 +169,8 @@ module Github merge_request.save!(validate: false) merge_request.merge_request_diffs.create - # Fetch review comments review_comments_url = "/repos/#{repo}/pulls/#{pull_request.iid}/comments" fetch_comments(merge_request, :review_comment, review_comments_url, LegacyDiffNote) - - # Fetch comments - comments_url = "/repos/#{repo}/issues/#{pull_request.iid}/comments" - fetch_comments(merge_request, :comment, comments_url) rescue => e error(:pull_request, pull_request.url, e.message) ensure @@ -241,12 +203,17 @@ module Github # for both features, like manipulating assignees, labels # and milestones, are provided within the Issues API. if representation.pull_request? - return unless representation.has_labels? + return unless representation.has_labels? || representation.has_comments? merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid) - merge_request.update_attribute(:label_ids, label_ids(representation.labels)) + + if representation.has_labels? + merge_request.update_attribute(:label_ids, label_ids(representation.labels)) + end + + fetch_comments_conditionally(merge_request, representation) else - return if Issue.where(iid: representation.iid, project_id: project.id).exists? + return if Issue.exists?(iid: representation.iid, project_id: project.id) author_id = user_id(representation.author, project.creator_id) issue = Issue.new @@ -263,17 +230,20 @@ module Github issue.updated_at = representation.updated_at issue.save!(validate: false) - # Fetch comments - if representation.has_comments? - comments_url = "/repos/#{repo}/issues/#{issue.iid}/comments" - fetch_comments(issue, :comment, comments_url) - end + fetch_comments_conditionally(issue, representation) end rescue => e error(:issue, representation.url, e.message) end end + def fetch_comments_conditionally(issuable, representation) + if representation.has_comments? + comments_url = "/repos/#{repo}/issues/#{issuable.iid}/comments" + fetch_comments(issuable, :comment, comments_url) + end + end + def fetch_comments(noteable, type, url, klass = Note) while url comments = Github::Client.new(options).get(url) diff --git a/lib/github/import/issue.rb b/lib/github/import/issue.rb new file mode 100644 index 00000000000..171f0872666 --- /dev/null +++ b/lib/github/import/issue.rb @@ -0,0 +1,13 @@ +module Github + class Import + class Issue < ::Issue + self.table_name = 'issues' + + self.reset_callbacks :save + self.reset_callbacks :create + self.reset_callbacks :commit + self.reset_callbacks :update + self.reset_callbacks :validate + end + end +end diff --git a/lib/github/import/legacy_diff_note.rb b/lib/github/import/legacy_diff_note.rb new file mode 100644 index 00000000000..18adff560b6 --- /dev/null +++ b/lib/github/import/legacy_diff_note.rb @@ -0,0 +1,12 @@ +module Github + class Import + class LegacyDiffNote < ::LegacyDiffNote + self.table_name = 'notes' + self.store_full_sti_class = false + + self.reset_callbacks :commit + self.reset_callbacks :update + self.reset_callbacks :validate + end + end +end diff --git a/lib/github/import/merge_request.rb b/lib/github/import/merge_request.rb new file mode 100644 index 00000000000..c258e5d5e0e --- /dev/null +++ b/lib/github/import/merge_request.rb @@ -0,0 +1,13 @@ +module Github + class Import + class MergeRequest < ::MergeRequest + self.table_name = 'merge_requests' + + self.reset_callbacks :create + self.reset_callbacks :save + self.reset_callbacks :commit + self.reset_callbacks :update + self.reset_callbacks :validate + end + end +end diff --git a/lib/github/import/note.rb b/lib/github/import/note.rb new file mode 100644 index 00000000000..8cf4f30e6b7 --- /dev/null +++ b/lib/github/import/note.rb @@ -0,0 +1,13 @@ +module Github + class Import + class Note < ::Note + self.table_name = 'notes' + self.store_full_sti_class = false + + self.reset_callbacks :save + self.reset_callbacks :commit + self.reset_callbacks :update + self.reset_callbacks :validate + end + end +end diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb index 3fde1b09efb..8e5c95f2287 100644 --- a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb +++ b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb @@ -3,11 +3,18 @@ module Gitlab class DeserializeMergeRequestDiffsAndCommits attr_reader :diff_ids, :commit_rows, :file_rows + class Error < StandardError + def backtrace + cause.backtrace + end + end + class MergeRequestDiff < ActiveRecord::Base self.table_name = 'merge_request_diffs' end BUFFER_ROWS = 1000 + DIFF_FILE_BUFFER_ROWS = 100 def perform(start_id, stop_id) merge_request_diffs = MergeRequestDiff @@ -26,13 +33,17 @@ module Gitlab if diff_ids.length > BUFFER_ROWS || commit_rows.length > BUFFER_ROWS || - file_rows.length > BUFFER_ROWS + file_rows.length > DIFF_FILE_BUFFER_ROWS flush_buffers! end end flush_buffers! + rescue => e + Rails.logger.info("#{self.class.name}: failed for IDs #{merge_request_diffs.map(&:id)} with #{e.class.name}") + + raise Error.new(e.inspect) end private @@ -45,17 +56,28 @@ module Gitlab def flush_buffers! if diff_ids.any? - MergeRequestDiff.transaction do - Gitlab::Database.bulk_insert('merge_request_diff_commits', commit_rows) - Gitlab::Database.bulk_insert('merge_request_diff_files', file_rows) + commit_rows.each_slice(BUFFER_ROWS).each do |commit_rows_slice| + bulk_insert('merge_request_diff_commits', commit_rows_slice) + end - MergeRequestDiff.where(id: diff_ids).update_all(st_commits: nil, st_diffs: nil) + file_rows.each_slice(DIFF_FILE_BUFFER_ROWS).each do |file_rows_slice| + bulk_insert('merge_request_diff_files', file_rows_slice) end + + MergeRequestDiff.where(id: diff_ids).update_all(st_commits: nil, st_diffs: nil) end reset_buffers! end + def bulk_insert(table, rows) + Gitlab::Database.bulk_insert(table, rows) + rescue ActiveRecord::RecordNotUnique + ids = rows.map { |row| row[:merge_request_diff_id] }.uniq.sort + + Rails.logger.info("#{self.class.name}: rows inserted twice for IDs #{ids}") + end + def single_diff_rows(merge_request_diff) sha_attribute = Gitlab::Database::ShaAttribute.new commits = YAML.load(merge_request_diff.st_commits) rescue [] diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 28bbf3b384e..d1979bb7ed3 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -149,16 +149,21 @@ module Gitlab description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author) description += pull_request.description + source_branch_sha = pull_request.source_branch_sha + target_branch_sha = pull_request.target_branch_sha + source_branch_sha = project.repository.commit(source_branch_sha)&.sha || source_branch_sha + target_branch_sha = project.repository.commit(target_branch_sha)&.sha || target_branch_sha + merge_request = project.merge_requests.create!( iid: pull_request.iid, title: pull_request.title, description: description, source_project: project, source_branch: pull_request.source_branch_name, - source_branch_sha: pull_request.source_branch_sha, + source_branch_sha: source_branch_sha, target_project: project, target_branch: pull_request.target_branch_name, - target_branch_sha: pull_request.target_branch_sha, + target_branch_sha: target_branch_sha, state: pull_request.state, author_id: gitlab_user_id(project, pull_request.author), assignee_id: nil, diff --git a/lib/gitlab/ci/build/policy.rb b/lib/gitlab/ci/build/policy.rb new file mode 100644 index 00000000000..d10cc7802d4 --- /dev/null +++ b/lib/gitlab/ci/build/policy.rb @@ -0,0 +1,15 @@ +module Gitlab + module Ci + module Build + module Policy + def self.fabricate(specs) + specifications = specs.to_h.map do |spec, value| + self.const_get(spec.to_s.camelize).new(value) + end + + specifications.compact + end + end + end + end +end diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb new file mode 100644 index 00000000000..b20d374288f --- /dev/null +++ b/lib/gitlab/ci/build/policy/kubernetes.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Build + module Policy + class Kubernetes < Policy::Specification + def initialize(spec) + unless spec.to_sym == :active + raise UnknownPolicyError + end + end + + def satisfied_by?(pipeline) + pipeline.has_kubernetes_active? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb new file mode 100644 index 00000000000..eadc0948d2f --- /dev/null +++ b/lib/gitlab/ci/build/policy/refs.rb @@ -0,0 +1,43 @@ +module Gitlab + module Ci + module Build + module Policy + class Refs < Policy::Specification + def initialize(refs) + @patterns = Array(refs) + end + + def satisfied_by?(pipeline) + @patterns.any? do |pattern| + pattern, path = pattern.split('@', 2) + + matches_path?(path, pipeline) && + matches_pattern?(pattern, pipeline) + end + end + + private + + def matches_path?(path, pipeline) + return true unless path + + pipeline.project_full_path == path + end + + def matches_pattern?(pattern, pipeline) + return true if pipeline.tag? && pattern == 'tags' + return true if pipeline.branch? && pattern == 'branches' + return true if pipeline.source == pattern + return true if pipeline.source&.pluralize == pattern + + if pattern.first == "/" && pattern.last == "/" + Regexp.new(pattern[1...-1]) =~ pipeline.ref + else + pattern == pipeline.ref + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb new file mode 100644 index 00000000000..c317291f29d --- /dev/null +++ b/lib/gitlab/ci/build/policy/specification.rb @@ -0,0 +1,25 @@ +module Gitlab + module Ci + module Build + module Policy + ## + # Abstract class that defines an interface of job policy + # specification. + # + # Used for job's only/except policy configuration. + # + class Specification + UnknownPolicyError = Class.new(StandardError) + + def initialize(spec) + @spec = spec + end + + def satisfied_by?(pipeline) + raise NotImplementedError + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb new file mode 100644 index 00000000000..8d82e1b288d --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/base.rb @@ -0,0 +1,27 @@ +module Gitlab + module Ci + module Pipeline + module Chain + class Base + attr_reader :pipeline, :project, :current_user + + def initialize(pipeline, command) + @pipeline = pipeline + @command = command + + @project = command.project + @current_user = command.current_user + end + + def perform! + raise NotImplementedError + end + + def break? + raise NotImplementedError + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb new file mode 100644 index 00000000000..d5e17a123df --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/create.rb @@ -0,0 +1,29 @@ +module Gitlab + module Ci + module Pipeline + module Chain + class Create < Chain::Base + include Chain::Helpers + + def perform! + ::Ci::Pipeline.transaction do + pipeline.save! + + @command.seeds_block&.call(pipeline) + + ::Ci::CreatePipelineStagesService + .new(project, current_user) + .execute(pipeline) + end + rescue ActiveRecord::RecordInvalid => e + error("Failed to persist the pipeline: #{e}") + end + + def break? + !pipeline.persisted? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb new file mode 100644 index 00000000000..02d81286f21 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/helpers.rb @@ -0,0 +1,25 @@ +module Gitlab + module Ci + module Pipeline + module Chain + module Helpers + def branch_exists? + return @is_branch if defined?(@is_branch) + + @is_branch = project.repository.branch_exists?(pipeline.ref) + end + + def tag_exists? + return @is_tag if defined?(@is_tag) + + @is_tag = project.repository.tag_exists?(pipeline.ref) + end + + def error(message) + pipeline.errors.add(:base, message) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb new file mode 100644 index 00000000000..015f2988327 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/sequence.rb @@ -0,0 +1,36 @@ +module Gitlab + module Ci + module Pipeline + module Chain + class Sequence + def initialize(pipeline, command, sequence) + @pipeline = pipeline + @completed = [] + + @sequence = sequence.map do |chain| + chain.new(pipeline, command) + end + end + + def build! + @sequence.each do |step| + step.perform! + + break if step.break? + + @completed << step + end + + @pipeline.tap do + yield @pipeline, self if block_given? + end + end + + def complete? + @completed.size == @sequence.size + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb new file mode 100644 index 00000000000..9a72de87bab --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/skip.rb @@ -0,0 +1,33 @@ +module Gitlab + module Ci + module Pipeline + module Chain + class Skip < Chain::Base + SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i + + def perform! + if skipped? + @pipeline.skip if @command.save_incompleted + end + end + + def skipped? + !@command.ignore_skip_ci && commit_message_skips_ci? + end + + def break? + skipped? + end + + private + + def commit_message_skips_ci? + return false unless @pipeline.git_commit_message + + @skipped ||= !!(@pipeline.git_commit_message =~ SKIP_PATTERN) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb new file mode 100644 index 00000000000..4913a604079 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb @@ -0,0 +1,54 @@ +module Gitlab + module Ci + module Pipeline + module Chain + module Validate + class Abilities < Chain::Base + include Gitlab::Allowable + include Chain::Helpers + + def perform! + unless project.builds_enabled? + return error('Pipelines are disabled!') + end + + unless allowed_to_trigger_pipeline? + if can?(current_user, :create_pipeline, project) + return error("Insufficient permissions for protected ref '#{pipeline.ref}'") + else + return error('Insufficient permissions to create a new pipeline') + end + end + end + + def break? + @pipeline.errors.any? + end + + def allowed_to_trigger_pipeline? + if current_user + allowed_to_create? + else # legacy triggers don't have a corresponding user + !project.protected_for?(@pipeline.ref) + end + end + + def allowed_to_create? + return unless can?(current_user, :create_pipeline, project) + + access = Gitlab::UserAccess.new(current_user, project: project) + + if branch_exists? + access.can_update_branch?(@pipeline.ref) + elsif tag_exists? + access.can_create_tag?(@pipeline.ref) + else + true # Allow it for now and we'll reject when we check ref existence + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb new file mode 100644 index 00000000000..489bcd79655 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb @@ -0,0 +1,35 @@ +module Gitlab + module Ci + module Pipeline + module Chain + module Validate + class Config < Chain::Base + include Chain::Helpers + + def perform! + unless @pipeline.config_processor + unless @pipeline.ci_yaml_file + return error("Missing #{@pipeline.ci_yaml_file_path} file") + end + + if @command.save_incompleted && @pipeline.has_yaml_errors? + @pipeline.drop + end + + return error(@pipeline.yaml_errors) + end + + unless @pipeline.has_stage_seeds? + return error('No stages / jobs for this pipeline.') + end + end + + def break? + @pipeline.errors.any? || @pipeline.persisted? + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb new file mode 100644 index 00000000000..70a4cfdbdea --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb @@ -0,0 +1,30 @@ +module Gitlab + module Ci + module Pipeline + module Chain + module Validate + class Repository < Chain::Base + include Chain::Helpers + + def perform! + unless branch_exists? || tag_exists? + return error('Reference not found') + end + + ## TODO, we check commit in the service, that is why + # there is no repository access here. + # + unless pipeline.sha + return error('Commit not found') + end + end + + def break? + @pipeline.errors.any? + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/duration.rb b/lib/gitlab/ci/pipeline/duration.rb new file mode 100644 index 00000000000..469fc094cc8 --- /dev/null +++ b/lib/gitlab/ci/pipeline/duration.rb @@ -0,0 +1,143 @@ +module Gitlab + module Ci + module Pipeline + # # Introduction - total running time + # + # The problem this module is trying to solve is finding the total running + # time amongst all the jobs, excluding retries and pending (queue) time. + # We could reduce this problem down to finding the union of periods. + # + # So each job would be represented as a `Period`, which consists of + # `Period#first` as when the job started and `Period#last` as when the + # job was finished. A simple example here would be: + # + # * A (1, 3) + # * B (2, 4) + # * C (6, 7) + # + # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4. + # C begins from 6, and ends to 7. Visually it could be viewed as: + # + # 0 1 2 3 4 5 6 7 + # AAAAAAA + # BBBBBBB + # CCCC + # + # The union of A, B, and C would be (1, 4) and (6, 7), therefore the + # total running time should be: + # + # (4 - 1) + (7 - 6) => 4 + # + # # The Algorithm + # + # The algorithm used here for union would be described as follow. + # First we make sure that all periods are sorted by `Period#first`. + # Then we try to merge periods by iterating through the first period + # to the last period. The goal would be merging all overlapped periods + # so that in the end all the periods are discrete. When all periods + # are discrete, we're free to just sum all the periods to get real + # running time. + # + # Here we begin from A, and compare it to B. We could find that + # before A ends, B already started. That is `B.first <= A.last` + # that is `2 <= 3` which means A and B are overlapping! + # + # When we found that two periods are overlapping, we would need to merge + # them into a new period and disregard the old periods. To make a new + # period, we take `A.first` as the new first because remember? we sorted + # them, so `A.first` must be smaller or equal to `B.first`. And we take + # `[A.last, B.last].max` as the new last because we want whoever ended + # later. This could be broken into two cases: + # + # 0 1 2 3 4 + # AAAAAAA + # BBBBBBB + # + # Or: + # + # 0 1 2 3 4 + # AAAAAAAAAA + # BBBB + # + # So that we need to take whoever ends later. Back to our example, + # after merging and discard A and B it could be visually viewed as: + # + # 0 1 2 3 4 5 6 7 + # DDDDDDDDDD + # CCCC + # + # Now we could go on and compare the newly created D and the old C. + # We could figure out that D and C are not overlapping by checking + # `C.first <= D.last` is `false`. Therefore we need to keep both C + # and D. The example would end here because there are no more jobs. + # + # After having the union of all periods, we just need to sum the length + # of all periods to get total time. + # + # (4 - 1) + (7 - 6) => 4 + # + # That is 4 is the answer in the example. + module Duration + extend self + + Period = Struct.new(:first, :last) do + def duration + last - first + end + end + + def from_pipeline(pipeline) + status = %w[success failed running canceled] + builds = pipeline.builds.latest + .where(status: status).where.not(started_at: nil).order(:started_at) + + from_builds(builds) + end + + def from_builds(builds) + now = Time.now + + periods = builds.map do |b| + Period.new(b.started_at, b.finished_at || now) + end + + from_periods(periods) + end + + # periods should be sorted by `first` + def from_periods(periods) + process_duration(process_periods(periods)) + end + + private + + def process_periods(periods) + return periods if periods.empty? + + periods.drop(1).inject([periods.first]) do |result, current| + previous = result.last + + if overlap?(previous, current) + result[-1] = merge(previous, current) + result + else + result << current + end + end + end + + def overlap?(previous, current) + current.first <= previous.last + end + + def merge(previous, current) + Period.new(previous.first, [previous.last, current.last].max) + end + + def process_duration(periods) + periods.sum(&:duration) + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline_duration.rb b/lib/gitlab/ci/pipeline_duration.rb deleted file mode 100644 index 3208cc2bef6..00000000000 --- a/lib/gitlab/ci/pipeline_duration.rb +++ /dev/null @@ -1,141 +0,0 @@ -module Gitlab - module Ci - # # Introduction - total running time - # - # The problem this module is trying to solve is finding the total running - # time amongst all the jobs, excluding retries and pending (queue) time. - # We could reduce this problem down to finding the union of periods. - # - # So each job would be represented as a `Period`, which consists of - # `Period#first` as when the job started and `Period#last` as when the - # job was finished. A simple example here would be: - # - # * A (1, 3) - # * B (2, 4) - # * C (6, 7) - # - # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4. - # C begins from 6, and ends to 7. Visually it could be viewed as: - # - # 0 1 2 3 4 5 6 7 - # AAAAAAA - # BBBBBBB - # CCCC - # - # The union of A, B, and C would be (1, 4) and (6, 7), therefore the - # total running time should be: - # - # (4 - 1) + (7 - 6) => 4 - # - # # The Algorithm - # - # The algorithm used here for union would be described as follow. - # First we make sure that all periods are sorted by `Period#first`. - # Then we try to merge periods by iterating through the first period - # to the last period. The goal would be merging all overlapped periods - # so that in the end all the periods are discrete. When all periods - # are discrete, we're free to just sum all the periods to get real - # running time. - # - # Here we begin from A, and compare it to B. We could find that - # before A ends, B already started. That is `B.first <= A.last` - # that is `2 <= 3` which means A and B are overlapping! - # - # When we found that two periods are overlapping, we would need to merge - # them into a new period and disregard the old periods. To make a new - # period, we take `A.first` as the new first because remember? we sorted - # them, so `A.first` must be smaller or equal to `B.first`. And we take - # `[A.last, B.last].max` as the new last because we want whoever ended - # later. This could be broken into two cases: - # - # 0 1 2 3 4 - # AAAAAAA - # BBBBBBB - # - # Or: - # - # 0 1 2 3 4 - # AAAAAAAAAA - # BBBB - # - # So that we need to take whoever ends later. Back to our example, - # after merging and discard A and B it could be visually viewed as: - # - # 0 1 2 3 4 5 6 7 - # DDDDDDDDDD - # CCCC - # - # Now we could go on and compare the newly created D and the old C. - # We could figure out that D and C are not overlapping by checking - # `C.first <= D.last` is `false`. Therefore we need to keep both C - # and D. The example would end here because there are no more jobs. - # - # After having the union of all periods, we just need to sum the length - # of all periods to get total time. - # - # (4 - 1) + (7 - 6) => 4 - # - # That is 4 is the answer in the example. - module PipelineDuration - extend self - - Period = Struct.new(:first, :last) do - def duration - last - first - end - end - - def from_pipeline(pipeline) - status = %w[success failed running canceled] - builds = pipeline.builds.latest - .where(status: status).where.not(started_at: nil).order(:started_at) - - from_builds(builds) - end - - def from_builds(builds) - now = Time.now - - periods = builds.map do |b| - Period.new(b.started_at, b.finished_at || now) - end - - from_periods(periods) - end - - # periods should be sorted by `first` - def from_periods(periods) - process_duration(process_periods(periods)) - end - - private - - def process_periods(periods) - return periods if periods.empty? - - periods.drop(1).inject([periods.first]) do |result, current| - previous = result.last - - if overlap?(previous, current) - result[-1] = merge(previous, current) - result - else - result << current - end - end - end - - def overlap?(previous, current) - current.first <= previous.last - end - - def merge(previous, current) - Period.new(previous.first, [previous.last, current.last].max) - end - - def process_duration(periods) - periods.sum(&:duration) - end - end - end -end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 7582964b24e..0bd78b03448 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -5,12 +5,11 @@ module Gitlab include Gitlab::Ci::Config::Entry::LegacyValidationHelpers - attr_reader :path, :cache, :stages, :jobs + attr_reader :cache, :stages, :jobs - def initialize(config, path = nil) + def initialize(config) @ci_config = Gitlab::Ci::Config.new(config) @config = @ci_config.to_hash - @path = path unless @ci_config.valid? raise ValidationError, @ci_config.errors.first @@ -21,28 +20,12 @@ module Gitlab raise ValidationError, e.message end - def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| - build_attributes(name) - end - end - def builds @jobs.map do |name, _| build_attributes(name) end end - def stage_seeds(pipeline) - seeds = @stages.uniq.map do |stage| - builds = pipeline_stage_builds(stage, pipeline) - - Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? - end - - seeds.compact - end - def build_attributes(name) job = @jobs[name.to_sym] || {} @@ -70,6 +53,32 @@ module Gitlab }.compact } end + def pipeline_stage_builds(stage, pipeline) + selected_jobs = @jobs.select do |_, job| + next unless job[:stage] == stage + + only_specs = Gitlab::Ci::Build::Policy + .fabricate(job.fetch(:only, {})) + except_specs = Gitlab::Ci::Build::Policy + .fabricate(job.fetch(:except, {})) + + only_specs.all? { |spec| spec.satisfied_by?(pipeline) } && + except_specs.none? { |spec| spec.satisfied_by?(pipeline) } + end + + selected_jobs.map { |_, job| build_attributes(job[:name]) } + end + + def stage_seeds(pipeline) + seeds = @stages.uniq.map do |stage| + builds = pipeline_stage_builds(stage, pipeline) + + Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? + end + + seeds.compact + end + def self.validation_message(content) return 'Please provide content of .gitlab-ci.yml' if content.blank? @@ -83,34 +92,6 @@ module Gitlab private - def pipeline_stage_builds(stage, pipeline) - builds = builds_for_stage_and_ref( - stage, pipeline.ref, pipeline.tag?, pipeline.source) - - builds.select do |build| - job = @jobs[build.fetch(:name).to_sym] - has_kubernetes = pipeline.has_kubernetes_active? - only_kubernetes = job.dig(:only, :kubernetes) - except_kubernetes = job.dig(:except, :kubernetes) - - [!only_kubernetes && !except_kubernetes, - only_kubernetes && has_kubernetes, - except_kubernetes && !has_kubernetes].any? - end - end - - def jobs_for_ref(ref, tag = false, source = nil) - @jobs.select do |_, job| - process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) - end - end - - def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_ref(ref, tag, source).select do |_, job| - job[:stage] == stage - end - end - def initial_parsing ## # Global config @@ -203,51 +184,6 @@ module Gitlab raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" end end - - def process?(only_params, except_params, ref, tag, source) - if only_params.present? - return false unless matching?(only_params, ref, tag, source) - end - - if except_params.present? - return false if matching?(except_params, ref, tag, source) - end - - true - end - - def matching?(patterns, ref, tag, source) - patterns.any? do |pattern| - pattern, path = pattern.split('@', 2) - matches_path?(path) && matches_pattern?(pattern, ref, tag, source) - end - end - - def matches_path?(path) - return true unless path - - path == self.path - end - - def matches_pattern?(pattern, ref, tag, source) - return true if tag && pattern == 'tags' - return true if !tag && pattern == 'branches' - return true if source_to_pattern(source) == pattern - - if pattern.first == "/" && pattern.last == "/" - Regexp.new(pattern[1...-1]) =~ ref - else - pattern == ref - end - end - - def source_to_pattern(source) - if %w[api external web].include?(source) - source - else - source&.pluralize - end - end end end end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index 58f86abc5c4..243c1f1394d 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -1,7 +1,7 @@ module Gitlab class ClosingIssueExtractor ISSUE_CLOSING_REGEX = begin - link_pattern = URI.regexp(%w(http https)) + link_pattern = Banzai::Filter::AutolinkFilter::LINK_PATTERN pattern = Gitlab.config.gitlab.issue_closing_pattern pattern = pattern.sub('%{issue_ref}', "(?:(?:#{link_pattern})|(?:#{Issue.reference_pattern}))") diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 4ab5b3455a5..31a46a738c3 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -64,8 +64,11 @@ module Gitlab # For performance purposes maximum 20 latest commits # will be passed as post receive hook data. - commit_attrs = commits_limited.map do |commit| - commit.hook_attrs(with_changed_files: true) + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38259 + commit_attrs = Gitlab::GitalyClient.allow_n_plus_1_calls do + commits_limited.map do |commit| + commit.hook_attrs(with_changed_files: true) + end end type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push' diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb index 371cbe04b9b..c98eefbce25 100644 --- a/lib/gitlab/diff/diff_refs.rb +++ b/lib/gitlab/diff/diff_refs.rb @@ -13,9 +13,9 @@ module Gitlab def ==(other) other.is_a?(self.class) && - base_sha == other.base_sha && - start_sha == other.start_sha && - head_sha == other.head_sha + shas_equal?(base_sha, other.base_sha) && + shas_equal?(start_sha, other.start_sha) && + shas_equal?(head_sha, other.head_sha) end alias_method :eql?, :== @@ -47,6 +47,22 @@ module Gitlab CompareService.new(project, head_sha).execute(project, start_sha, straight: straight) end end + + private + + def shas_equal?(sha1, sha2) + return true if sha1 == sha2 + return false if sha1.nil? || sha2.nil? + return false unless sha1.class == sha2.class + + length = [sha1.length, sha2.length].min + + # If either of the shas is below the minimum length, we cannot be sure + # that they actually refer to the same commit because of hash collision. + return false if length < Commit::MIN_SHA_LENGTH + + sha1[0, length] == sha2[0, length] + end end end end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 1dabd4ebdd0..fcac85ff892 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -5,7 +5,7 @@ module Gitlab delegate :new_file?, :deleted_file?, :renamed_file?, :old_path, :new_path, :a_mode, :b_mode, :mode_changed?, - :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, to: :diff, prefix: false + :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, :has_binary_notice?, to: :diff, prefix: false # Finding a viewer for a diff file happens based only on extension and whether the # diff file blobs are binary or text, which means 1 diff file should only be matched by 1 viewer, @@ -166,7 +166,7 @@ module Gitlab end def binary? - old_blob&.binary? || new_blob&.binary? + has_binary_notice? || old_blob&.binary? || new_blob&.binary? end def text? diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index a6007ebf531..88ae65cb468 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -22,7 +22,10 @@ module Gitlab end def diff_files - @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37445 + Gitlab::GitalyClient.allow_n_plus_1_calls do + @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } + end end def diff_file_with_old_path(old_path) diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index f80afb20f0c..b8db3adef0a 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -49,12 +49,13 @@ module Gitlab coder['attributes'] = self.to_h end - def key - @key ||= [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || ""), old_line, new_line] - end - def ==(other) - other.is_a?(self.class) && key == other.key + other.is_a?(self.class) && + other.diff_refs == diff_refs && + other.old_path == old_path && + other.new_path == new_path && + other.old_line == old_line && + other.new_line == new_line end def to_h diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index abd401224d8..c5a8ea12245 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -284,13 +284,18 @@ module Gitlab EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch was found in the EE repository. + If you're a community contributor, don't worry, someone from + GitLab Inc. will take care of this, and you don't have to do anything. + If you're willing to help, and are ok to contribute to EE as well, + you're welcome to help. You could follow the instructions below. + #{conflicting_files_msg} We advise you to create a `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch that includes changes from `#{ce_branch}` but also specific changes than can be applied cleanly to EE/master. In some cases, the conflicts are trivial and you can ignore the warning from this job. As always, - use your best judgment! + use your best judgement! There are different ways to create such branch: diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index b984492d369..455814a9159 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -29,6 +29,8 @@ module Gitlab # http://gitlab.com/some/link/#1234, and code `puts #1234`' # class ReferenceRewriter + RewriteError = Class.new(StandardError) + def initialize(text, source_project, current_user) @text = text @source_project = source_project @@ -61,6 +63,10 @@ module Gitlab cross_reference = build_cross_reference(referable, target_project) return reference if reference == cross_reference + if cross_reference.nil? + raise RewriteError, "Unspecified reference detected for #{referable.class.name}" + end + new_text = before + cross_reference + after substitution_valid?(new_text) ? cross_reference : reference end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index b4b6326cfdd..c78fe63f9b5 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -57,6 +57,15 @@ module Gitlab def version Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first) end + + def check_namespace!(*objects) + expected_namespace = self.name + '::' + objects.each do |object| + unless object.class.name.start_with?(expected_namespace) + raise ArgumentError, "expected object in #{expected_namespace}, got #{object}" + end + end + end end end end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 8d96826f6ee..a4336facee5 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -32,6 +32,8 @@ module Gitlab else blob = repository.lookup(sha) + next unless blob.is_a?(Rugged::Blob) + new( id: blob.oid, size: blob.size, diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 1f370686186..1957c254c28 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -413,6 +413,10 @@ module Gitlab end end + def merge_commit? + parent_ids.size > 1 + end + private def init_from_hash(hash) diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index a23c8cf0dd1..096301d300f 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -206,6 +206,10 @@ module Gitlab Diff.binary_message(@old_path, @new_path) end + def has_binary_notice? + @diff.start_with?('Binary') + end + private def init_from_rugged(rugged) diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index cc35d77c6e4..208e4bbaf60 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -83,13 +83,14 @@ module Gitlab def call_update_hook(gl_id, oldrev, newrev, ref) Dir.chdir(repo_path) do stdout, stderr, status = Open3.capture3({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev) - [status.success?, stderr.presence || stdout] + [status.success?, (stderr.presence || stdout).gsub(/\R/, "<br>").html_safe] end end def retrieve_error_message(stderr, stdout) - err_message = stderr.gets - err_message.blank? ? stdout.gets : err_message + err_message = stderr.read + err_message = err_message.blank? ? stdout.read : err_message + err_message.gsub(/\R/, "<br>").html_safe end end end diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index dcdec818f5e..786e2e7e8dc 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -1,6 +1,8 @@ module Gitlab module Git class OperationService + include Gitlab::Git::Popen + WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do alias_method :repo_created?, :repo_created alias_method :branch_created?, :branch_created @@ -15,9 +17,7 @@ module Gitlab end # Refactoring aid - unless new_repository.is_a?(Gitlab::Git::Repository) - raise "expected a Gitlab::Git::Repository, got #{new_repository}" - end + Gitlab::Git.check_namespace!(new_repository) @repository = new_repository end @@ -152,7 +152,7 @@ module Gitlab # (and have!) accidentally reset the ref to an earlier state, clobbering # commits. See also https://github.com/libgit2/libgit2/issues/1534. command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] - _, status = Gitlab::Popen.popen( + _, status = popen( command, repository.path) do |stdin| stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb index 25fa62ce4bd..3d2fc471d28 100644 --- a/lib/gitlab/git/popen.rb +++ b/lib/gitlab/git/popen.rb @@ -5,17 +5,21 @@ require 'open3' module Gitlab module Git module Popen - def popen(cmd, path) + def popen(cmd, path, vars = {}) unless cmd.is_a?(Array) raise "System commands must be given as an array of strings" end - vars = { "PWD" => path } + path ||= Dir.pwd + vars['PWD'] = path options = { chdir: path } @cmd_output = "" @cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + yield(stdin) if block_given? + stdin.close + @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index c499ff101b5..ef76245a608 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -19,13 +19,12 @@ module Gitlab InvalidRef = Class.new(StandardError) GitError = Class.new(StandardError) DeleteBranchError = Class.new(StandardError) + CreateTreeError = Class.new(StandardError) + TagExistsError = Class.new(StandardError) class << self - # Unlike `new`, `create` takes the storage path, not the storage name - def create(storage_path, name, bare: true, symlink_hooks_to: nil) - repo_path = File.join(storage_path, name) - repo_path += '.git' unless repo_path.end_with?('.git') - + # Unlike `new`, `create` takes the repository path + def create(repo_path, bare: true, symlink_hooks_to: nil) FileUtils.mkdir_p(repo_path, mode: 0770) # Equivalent to `git --git-path=#{repo_path} init [--bare]` @@ -72,8 +71,6 @@ module Gitlab delegate :empty?, to: :rugged - delegate :exists?, to: :gitaly_repository_client - def ==(other) path == other.path end @@ -101,6 +98,18 @@ module Gitlab @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage) end + def exists? + Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| + if enabled + gitaly_repository_client.exists? + else + circuit_breaker.perform do + File.exist?(File.join(@path, 'refs')) + end + end + end + end + # Returns an Array of branch names # sorted by name ASC def branch_names @@ -180,6 +189,28 @@ module Gitlab end end + def has_local_branches? + gitaly_migrate(:has_local_branches) do |is_enabled| + if is_enabled + gitaly_ref_client.has_local_branches? + else + has_local_branches_rugged? + end + end + end + + def has_local_branches_rugged? + rugged.branches.each(:local).any? do |ref| + begin + ref.name && ref.target # ensures the branch is valid + + true + rescue Rugged::ReferenceError + false + end + end + end + # Returns the number of valid tags def tag_count gitaly_migrate(:tag_names) do |is_enabled| @@ -385,7 +416,13 @@ module Gitlab options[:limit] ||= 0 options[:offset] ||= 0 - raw_log(options).map { |c| Commit.decorate(self, c) } + gitaly_migrate(:find_commits) do |is_enabled| + if is_enabled + gitaly_commit_client.find_commits(options) + else + raw_log(options).map { |c| Commit.decorate(self, c) } + end + end end # Used in gitaly-ruby @@ -474,7 +511,15 @@ module Gitlab # diff options. The +options+ hash can also include :break_rewrites to # split larger rewrites into delete/add pairs. def diff(from, to, options = {}, *paths) - Gitlab::Git::DiffCollection.new(diff_patches(from, to, options, *paths), options) + iterator = gitaly_migrate(:diff_between) do |is_enabled| + if is_enabled + gitaly_commit_client.diff(from, to, options.merge(paths: paths)) + else + diff_patches(from, to, options, *paths) + end + end + + Gitlab::Git::DiffCollection.new(iterator, options) end # Returns a RefName for a given SHA @@ -489,7 +534,7 @@ module Gitlab # Not found -> ["", 0] # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] - Gitlab::Popen.popen(args, @path).first.split.last + popen(args, @path).first.split.last end end end @@ -621,24 +666,13 @@ module Gitlab end def add_tag(tag_name, user:, target:, message: nil) - target_object = Ref.dereference_object(lookup(target)) - raise InvalidRef.new("target not found: #{target}") unless target_object - - user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id) - - options = nil # Use nil, not the empty hash. Rugged cares about this. - if message - options = { - message: message, - tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name) - } + gitaly_migrate(:operation_user_add_tag) do |is_enabled| + if is_enabled + gitaly_add_tag(tag_name, user: user, target: target, message: message) + else + rugged_add_tag(tag_name, user: user, target: target, message: message) + end end - - OperationService.new(user, self).add_tag(tag_name, target_object.oid, options) - - find_tag(tag_name) - rescue Rugged::ReferenceError => ex - raise InvalidRef, ex end def rm_branch(branch_name, user:) @@ -646,7 +680,13 @@ module Gitlab end def rm_tag(tag_name, user:) - OperationService.new(user, self).rm_tag(find_tag(tag_name)) + gitaly_migrate(:operation_user_delete_tag) do |is_enabled| + if is_enabled + gitaly_operations_client.rm_tag(tag_name, user) + else + Gitlab::Git::OperationService.new(user, self).rm_tag(find_tag(tag_name)) + end + end end def find_tag(name) @@ -684,6 +724,88 @@ module Gitlab nil end + def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) + OperationService.new(user, self).with_branch( + branch_name, + start_branch_name: start_branch_name, + start_repository: start_repository + ) do |start_commit| + + Gitlab::Git.check_namespace!(commit, start_repository) + + revert_tree_id = check_revert_content(commit, start_commit.sha) + raise CreateTreeError unless revert_tree_id + + committer = user_to_committer(user) + + create_commit(message: message, + author: committer, + committer: committer, + tree: revert_tree_id, + parents: [start_commit.sha]) + end + end + + def check_revert_content(target_commit, source_sha) + args = [target_commit.sha, source_sha] + args << { mainline: 1 } if target_commit.merge_commit? + + revert_index = rugged.revert_commit(*args) + return false if revert_index.conflicts? + + tree_id = revert_index.write_tree(rugged) + return false unless diff_exists?(source_sha, tree_id) + + tree_id + end + + def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) + OperationService.new(user, self).with_branch( + branch_name, + start_branch_name: start_branch_name, + start_repository: start_repository + ) do |start_commit| + + Gitlab::Git.check_namespace!(commit, start_repository) + + cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) + raise CreateTreeError unless cherry_pick_tree_id + + committer = user_to_committer(user) + + create_commit(message: message, + author: { + email: commit.author_email, + name: commit.author_name, + time: commit.authored_date + }, + committer: committer, + tree: cherry_pick_tree_id, + parents: [start_commit.sha]) + end + end + + def check_cherry_pick_content(target_commit, source_sha) + args = [target_commit.sha, source_sha] + args << 1 if target_commit.merge_commit? + + cherry_pick_index = rugged.cherrypick_commit(*args) + return false if cherry_pick_index.conflicts? + + tree_id = cherry_pick_index.write_tree(rugged) + return false unless diff_exists?(source_sha, tree_id) + + tree_id + end + + def diff_exists?(sha1, sha2) + rugged.diff(sha1, sha2).size > 0 + end + + def user_to_committer(user) + Gitlab::Git.committer_hash(email: user.email, name: user.name) + end + def create_commit(params = {}) params[:message].delete!("\r") @@ -709,9 +831,7 @@ module Gitlab end command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] - message, status = Gitlab::Popen.popen( - command, - path) do |stdin| + message, status = popen(command, path) do |stdin| stdin.write(instructions.join) end @@ -835,14 +955,18 @@ module Gitlab end def with_repo_branch_commit(start_repository, start_branch_name) - raise "expected Gitlab::Git::Repository, got #{start_repository}" unless start_repository.is_a?(Gitlab::Git::Repository) + Gitlab::Git.check_namespace!(start_repository) return yield nil if start_repository.empty_repo? if start_repository == self yield commit(start_branch_name) else - sha = start_repository.commit(start_branch_name).sha + start_commit = start_repository.commit(start_branch_name) + + return yield nil unless start_commit + + sha = start_commit.sha if branch_commit = commit(sha) yield branch_commit @@ -871,8 +995,9 @@ module Gitlab with_repo_branch_commit(source_repository, source_branch) do |commit| if commit write_ref(local_ref, commit.sha) + true else - raise Rugged::ReferenceError, 'source repository is empty' + false end end end @@ -931,13 +1056,19 @@ module Gitlab # This method return true if repository contains some content visible in project page. # def has_visible_content? - branch_count > 0 + return @has_visible_content if defined?(@has_visible_content) + + @has_visible_content = has_local_branches? end def gitaly_repository Gitlab::GitalyClient::Util.repository(@storage, @relative_path) end + def gitaly_operations_client + @gitaly_operations_client ||= Gitlab::GitalyClient::OperationService.new(self) + end + def gitaly_ref_client @gitaly_ref_client ||= Gitlab::GitalyClient::RefService.new(self) end @@ -950,8 +1081,8 @@ module Gitlab @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self) end - def gitaly_migrate(method, &block) - Gitlab::GitalyClient.migrate(method, &block) + def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block) + Gitlab::GitalyClient.migrate(method, status: status, &block) rescue GRPC::NotFound => e raise NoRepository.new(e) rescue GRPC::BadStatus => e @@ -962,14 +1093,17 @@ module Gitlab # Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'. def branches_filter(filter: nil, sort_by: nil) - branches = rugged.branches.each(filter).map do |rugged_ref| - begin - target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) - Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) - rescue Rugged::ReferenceError - # Omit invalid branch - end - end.compact + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464 + branches = Gitlab::GitalyClient.allow_n_plus_1_calls do + rugged.branches.each(filter).map do |rugged_ref| + begin + target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) + Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) + rescue Rugged::ReferenceError + # Omit invalid branch + end + end.compact + end sort_branches(branches, sort_by) end @@ -1269,6 +1403,33 @@ module Gitlab false end + def gitaly_add_tag(tag_name, user:, target:, message: nil) + gitaly_operations_client.add_tag(tag_name, user, target, message) + end + + def rugged_add_tag(tag_name, user:, target:, message: nil) + target_object = Ref.dereference_object(lookup(target)) + raise InvalidRef.new("target not found: #{target}") unless target_object + + user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id) + + options = nil # Use nil, not the empty hash. Rugged cares about this. + if message + options = { + message: message, + tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name) + } + end + + Gitlab::Git::OperationService.new(user, self).add_tag(tag_name, target_object.oid, options) + + find_tag(tag_name) + rescue Rugged::ReferenceError => ex + raise InvalidRef, ex + rescue Rugged::TagError + raise TagExistsError + end + def rugged_create_branch(ref, start_point) rugged_ref = rugged.branches.create(ref, start_point) target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index 2b5785a1f08..e0943d3a3eb 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -3,6 +3,8 @@ module Gitlab module Git class RevList + include Gitlab::Git::Popen + attr_reader :oldrev, :newrev, :path_to_repo def initialize(path_to_repo:, newrev:, oldrev: nil) @@ -26,7 +28,7 @@ module Gitlab private def execute(args) - output, status = Gitlab::Popen.popen(args, nil, Gitlab::Git::Env.all.stringify_keys) + output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys) unless status.zero? raise "Got a non-zero exit code while calling out `#{args.join(' ')}`." diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb index e28be4b8a38..08e6c29abad 100644 --- a/lib/gitlab/git/storage.rb +++ b/lib/gitlab/git/storage.rb @@ -11,6 +11,7 @@ module Gitlab end CircuitOpen = Class.new(Inaccessible) + Misconfiguration = Class.new(Inaccessible) REDIS_KEY_PREFIX = 'storage_accessible:'.freeze diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index 9ea9367d4b7..1eaa2d83fb6 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -28,14 +28,26 @@ module Gitlab def self.for_storage(storage) cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do Hash.new do |hash, storage_name| - hash[storage_name] = new(storage_name) + hash[storage_name] = build(storage_name) end end cached_circuitbreakers[storage] end - def initialize(storage, hostname = Gitlab::Environment.hostname) + def self.build(storage, hostname = Gitlab::Environment.hostname) + config = Gitlab.config.repositories.storages[storage] + + if !config.present? + NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured")) + elsif !config['path'].present? + NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured")) + else + new(storage, hostname) + end + end + + def initialize(storage, hostname) @storage = storage @hostname = hostname @@ -64,6 +76,10 @@ module Gitlab recent_failure || too_many_failures end + def failure_info + @failure_info ||= get_failure_info + end + # Memoizing the `storage_available` call means we only do it once per # request when the storage is available. # @@ -121,10 +137,12 @@ module Gitlab end end - def failure_info - @failure_info ||= get_failure_info + def cache_key + @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" end + private + def get_failure_info last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis| redis.hmget(cache_key, :last_failure, :failure_count) @@ -134,10 +152,6 @@ module Gitlab FailureInfo.new(last_failure, failure_count.to_i) end - - def cache_key - @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" - end end end end diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb index 2d723147f4f..1564e94b7f7 100644 --- a/lib/gitlab/git/storage/health.rb +++ b/lib/gitlab/git/storage/health.rb @@ -78,7 +78,7 @@ module Gitlab def failing_circuit_breakers @failing_circuit_breakers ||= failing_on_hosts.map do |hostname| - CircuitBreaker.new(storage_name, hostname) + CircuitBreaker.build(storage_name, hostname) end end diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb new file mode 100644 index 00000000000..297c043d054 --- /dev/null +++ b/lib/gitlab/git/storage/null_circuit_breaker.rb @@ -0,0 +1,47 @@ +module Gitlab + module Git + module Storage + class NullCircuitBreaker + # These will have actual values + attr_reader :storage, + :hostname + + # These will always have nil values + attr_reader :storage_path, + :failure_wait_time, + :failure_reset_time, + :storage_timeout + + def initialize(storage, hostname, error: nil) + @storage = storage + @hostname = hostname + @error = error + end + + def perform + @error ? raise(@error) : yield + end + + def circuit_broken? + !!@error + end + + def failure_count_threshold + 1 + end + + def last_failure + circuit_broken? ? Time.now : nil + end + + def failure_count + circuit_broken? ? 1 : 0 + end + + def failure_info + Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(last_failure, failure_count) + end + end + end + end +end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 62d1ecae676..db67ede9d9e 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -16,7 +16,8 @@ module Gitlab account_blocked: 'Your account has been blocked.', command_not_allowed: "The command you're trying to execute is not allowed.", upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.', - receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.' + receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.', + readonly: 'The repository is temporarily read-only. Please try again later.' }.freeze DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze @@ -159,6 +160,10 @@ module Gitlab end def check_push_access!(changes) + if project.repository_read_only? + raise UnauthorizedError, ERROR_MESSAGES[:readonly] + end + if deploy_key check_deploy_key_push_access! elsif user diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index a3dc2cd0b60..955d2307f88 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -10,7 +10,24 @@ module Gitlab OPT_OUT = 3 end + class TooManyInvocationsError < StandardError + attr_reader :call_site, :invocation_count, :max_call_stack + + def initialize(call_site, invocation_count, max_call_stack, most_invoked_stack) + @call_site = call_site + @invocation_count = invocation_count + @max_call_stack = max_call_stack + stacks = most_invoked_stack.join('\n') if most_invoked_stack + + msg = "GitalyClient##{call_site} called #{invocation_count} times from single request. Potential n+1?" + msg << "\nThe following call site called into Gitaly #{max_call_stack} times:\n#{stacks}\n" if stacks + + super(msg) + end + end + SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze + MAXIMUM_GITALY_CALLS = 30 MUTEX = Mutex.new private_constant :MUTEX @@ -53,6 +70,8 @@ module Gitlab # All Gitaly RPC call sites should use GitalyClient.call. This method # makes sure that per-request authentication headers are set. def self.call(storage, service, rpc, request) + enforce_gitaly_request_limits(:call) + metadata = request_metadata(storage) metadata = yield(metadata) if block_given? stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend @@ -107,12 +126,100 @@ module Gitlab private_class_method :opt_into_all_features? def self.migrate(feature, status: MigrationStatus::OPT_IN) + # Enforce limits at both the `migrate` and `call` sites to ensure that + # problems are not hidden by a feature being disabled + enforce_gitaly_request_limits(:migrate) + is_enabled = feature_enabled?(feature, status: status) metric_name = feature.to_s metric_name += "_gitaly" if is_enabled Gitlab::Metrics.measure(metric_name) do - yield is_enabled + # Some migrate calls wrap other migrate calls + allow_n_plus_1_calls do + yield is_enabled + end + end + end + + # Ensures that Gitaly is not being abuse through n+1 misuse etc + def self.enforce_gitaly_request_limits(call_site) + # Only count limits in request-response environments (not sidekiq for example) + return unless RequestStore.active? + + # This is this actual number of times this call was made. Used for information purposes only + actual_call_count = increment_call_count("gitaly_#{call_site}_actual") + + # Do no enforce limits in production + return if Rails.env.production? + + # Check if this call is nested within a allow_n_plus_1_calls + # block and skip check if it is + return if get_call_count(:gitaly_call_count_exception_block_depth) > 0 + + # This is the count of calls outside of a `allow_n_plus_1_calls` block + # It is used for enforcement but not statistics + permitted_call_count = increment_call_count("gitaly_#{call_site}_permitted") + + count_stack + + return if permitted_call_count <= MAXIMUM_GITALY_CALLS + + raise TooManyInvocationsError.new(call_site, actual_call_count, max_call_count, max_stacks) + end + + def self.allow_n_plus_1_calls + return yield unless RequestStore.active? + + begin + increment_call_count(:gitaly_call_count_exception_block_depth) + yield + ensure + decrement_call_count(:gitaly_call_count_exception_block_depth) + end + end + + def self.get_call_count(key) + RequestStore.store[key] || 0 + end + private_class_method :get_call_count + + def self.increment_call_count(key) + RequestStore.store[key] ||= 0 + RequestStore.store[key] += 1 + end + private_class_method :increment_call_count + + def self.decrement_call_count(key) + RequestStore.store[key] -= 1 + end + private_class_method :decrement_call_count + + # Returns an estimate of the number of Gitaly calls made for this + # request + def self.get_request_count + return 0 unless RequestStore.active? + + gitaly_migrate_count = get_call_count("gitaly_migrate_actual") + gitaly_call_count = get_call_count("gitaly_call_actual") + + # Using the maximum of migrate and call_count will provide an + # indicator of how many Gitaly calls will be made, even + # before a feature is enabled. This provides us with a single + # metric, but not an exact number, but this tradeoff is acceptable + if gitaly_migrate_count > gitaly_call_count + gitaly_migrate_count + else + gitaly_call_count + end + end + + def self.reset_counts + return unless RequestStore.active? + + %w[migrate call].each do |call_site| + RequestStore.store["gitaly_#{call_site}_actual"] = 0 + RequestStore.store["gitaly_#{call_site}_permitted"] = 0 end end @@ -121,8 +228,54 @@ module Gitlab path.read.chomp end + def self.timestamp(t) + Google::Protobuf::Timestamp.new(seconds: t.to_i) + end + def self.encode(s) s.dup.force_encoding(Encoding::ASCII_8BIT) end + + def self.encode_repeated(a) + Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } ) + end + + # Count a stack. Used for n+1 detection + def self.count_stack + return unless RequestStore.active? + + stack_string = caller.drop(1).join("\n") + + RequestStore.store[:stack_counter] ||= Hash.new + + count = RequestStore.store[:stack_counter][stack_string] || 0 + RequestStore.store[:stack_counter][stack_string] = count + 1 + end + private_class_method :count_stack + + # Returns a count for the stack which called Gitaly the most times. Used for n+1 detection + def self.max_call_count + return 0 unless RequestStore.active? + + stack_counter = RequestStore.store[:stack_counter] + return 0 unless stack_counter + + stack_counter.values.max + end + private_class_method :max_call_count + + # Returns the stacks that calls Gitaly the most times. Used for n+1 detection + def self.max_stacks + return nil unless RequestStore.active? + + stack_counter = RequestStore.store[:stack_counter] + return nil unless stack_counter + + max = max_call_count + return nil if max.zero? + + stack_counter.select { |_, v| v == max }.keys + end + private_class_method :max_stacks end end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 1ba1a7830a4..36da63fd586 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -32,20 +32,38 @@ module Gitlab GitalyClient.call(@repository.storage, :commit_service, :commit_is_ancestor, request).value end + def diff(from, to, options = {}) + from_id = case from + when NilClass + EMPTY_TREE_ID + when Rugged::Commit + from.oid + else + from + end + + to_id = case to + when NilClass + EMPTY_TREE_ID + when Rugged::Commit + to.oid + else + to + end + + request_params = diff_between_commits_request_params(from_id, to_id, options) + + call_commit_diff(request_params, options) + end + def diff_from_parent(commit, options = {}) - request_params = commit_diff_request_params(commit, options) - request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false) - request_params[:enforce_limits] = options.fetch(:limits, true) - request_params[:collapse_diffs] = request_params[:enforce_limits] || !options.fetch(:expanded, true) - request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h) + request_params = diff_from_parent_request_params(commit, options) - request = Gitaly::CommitDiffRequest.new(request_params) - response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request) - GitalyClient::DiffStitcher.new(response) + call_commit_diff(request_params, options) end def commit_deltas(commit) - request = Gitaly::CommitDeltaRequest.new(commit_diff_request_params(commit)) + request = Gitaly::CommitDeltaRequest.new(diff_from_parent_request_params(commit)) response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request) response.flat_map { |msg| msg.deltas } @@ -212,16 +230,51 @@ module Gitlab GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request) end + def find_commits(options) + request = Gitaly::FindCommitsRequest.new( + repository: @gitaly_repo, + limit: options[:limit], + offset: options[:offset], + follow: options[:follow], + skip_merges: options[:skip_merges], + disable_walk: options[:disable_walk] + ) + request.after = GitalyClient.timestamp(options[:after]) if options[:after] + request.before = GitalyClient.timestamp(options[:before]) if options[:before] + request.revision = GitalyClient.encode(options[:ref]) if options[:ref] + + request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present? + + response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request) + + consume_commits_response(response) + end + private - def commit_diff_request_params(commit, options = {}) + def call_commit_diff(request_params, options = {}) + request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false) + request_params[:enforce_limits] = options.fetch(:limits, true) + request_params[:collapse_diffs] = request_params[:enforce_limits] || !options.fetch(:expanded, true) + request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h) + + request = Gitaly::CommitDiffRequest.new(request_params) + response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request) + GitalyClient::DiffStitcher.new(response) + end + + def diff_from_parent_request_params(commit, options = {}) parent_id = commit.parent_ids.first || EMPTY_TREE_ID + diff_between_commits_request_params(parent_id, commit.id, options) + end + + def diff_between_commits_request_params(from_id, to_id, options) { repository: @gitaly_repo, - left_commit_id: parent_id, - right_commit_id: commit.id, - paths: options.fetch(:paths, []) + left_commit_id: from_id, + right_commit_id: to_id, + paths: options.fetch(:paths, []).map { |path| GitalyClient.encode(path) } } end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb new file mode 100644 index 00000000000..2d5440e7ea8 --- /dev/null +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -0,0 +1,45 @@ +module Gitlab + module GitalyClient + class OperationService + def initialize(repository) + @gitaly_repo = repository.gitaly_repository + @repository = repository + end + + def rm_tag(tag_name, user) + request = Gitaly::UserDeleteTagRequest.new( + repository: @gitaly_repo, + tag_name: GitalyClient.encode(tag_name), + user: Util.gitaly_user(user) + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request) + + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + end + end + + def add_tag(tag_name, user, target, message) + request = Gitaly::UserCreateTagRequest.new( + repository: @gitaly_repo, + user: Util.gitaly_user(user), + tag_name: GitalyClient.encode(tag_name), + target_revision: GitalyClient.encode(target), + message: GitalyClient.encode(message.to_s) + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request) + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + elsif response.exists + raise Gitlab::Git::Repository::TagExistsError + end + + Util.gitlab_tag_from_gitaly_tag(@repository, response.tag) + rescue GRPC::FailedPrecondition => e + raise Gitlab::Git::Repository::InvalidRef, e + end + end + end +end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 8ef873d5848..8214b7d63fa 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -57,6 +57,14 @@ module Gitlab branch_names.count end + # TODO implement a more efficient RPC for this https://gitlab.com/gitlab-org/gitaly/issues/616 + def has_local_branches? + request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo) + response = GitalyClient.call(@storage, :ref_service, :find_all_branch_names, request).first + + response&.names.present? + end + def local_branches(sort_by: nil) request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo) request.sort_by = sort_by_param(sort_by) if sort_by @@ -155,19 +163,7 @@ module Gitlab def consume_tags_response(response) response.flat_map do |message| - message.tags.map do |gitaly_tag| - if gitaly_tag.target_commit.present? - gitaly_commit = Gitlab::Git::Commit.decorate(@repository, gitaly_tag.target_commit) - end - - Gitlab::Git::Tag.new( - @repository, - encode!(gitaly_tag.name.dup), - gitaly_tag.id, - gitaly_commit, - encode!(gitaly_tag.message.chomp) - ) - end + message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) } end end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 177a1284f38..fdf912214e0 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -53,6 +53,11 @@ module Gitlab GitalyClient.call(@storage, :repository_service, :fetch_remote, request) end + + def create_repository + request = Gitaly::CreateRepositoryRequest.new(repository: @gitaly_repo) + GitalyClient.call(@storage, :repository_service, :create_repository, request) + end end end end diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index 8fc937496af..2fb5875a7a2 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -10,6 +10,30 @@ module Gitlab git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']) ) end + + def gitaly_user(gitlab_user) + return unless gitlab_user + + Gitaly::User.new( + gl_id: Gitlab::GlId.gl_id(gitlab_user), + name: GitalyClient.encode(gitlab_user.name), + email: GitalyClient.encode(gitlab_user.email) + ) + end + + def gitlab_tag_from_gitaly_tag(repository, gitaly_tag) + if gitaly_tag.target_commit.present? + commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit) + end + + Gitlab::Git::Tag.new( + repository, + Gitlab::EncodingHelper.encode!(gitaly_tag.name.dup), + gitaly_tag.id, + commit, + Gitlab::EncodingHelper.encode!(gitaly_tag.message.chomp) + ) + end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 9bcc579278f..3a666c2268b 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -20,6 +20,7 @@ module Gitlab gon.gitlab_url = Gitlab.config.gitlab.url gon.revision = Gitlab::REVISION gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') + gon.sprite_icons = ActionController::Base.helpers.asset_path('icons.svg') if current_user gon.current_user_id = current_user.id diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index eef97f54962..afaa59b1018 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -58,11 +58,11 @@ module Gitlab end def repository_storages - @repository_storage ||= Gitlab::CurrentSettings.current_application_settings.repository_storages + storages_paths.keys end def storages_paths - @storage_paths ||= Gitlab.config.repositories.storages + Gitlab.config.repositories.storages end def exec_with_timeout(cmd_args, *args, &block) @@ -125,7 +125,7 @@ module Gitlab end def storage_circuitbreaker_test(storage_name) - Gitlab::Git::Storage::CircuitBreaker.new(storage_name).perform { "OK" } + Gitlab::Git::Storage::CircuitBreaker.build(storage_name).perform { "OK" } rescue Gitlab::Git::Storage::Inaccessible nil end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index fb68627dedf..e60ceba27c8 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -16,7 +16,7 @@ module Gitlab def self.allowed?(user) self.open(user) do |access| if access.allowed? - Users::UpdateService.new(user, last_credential_check_at: Time.now).execute + Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute true else diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb deleted file mode 100644 index 306923902e0..00000000000 --- a/lib/gitlab/markdown/pipeline.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Gitlab - module Markdown - class Pipeline - def self.[](name) - name ||= :full - const_get("#{name.to_s.camelize}Pipeline") - end - - def self.filters - [] - end - - def self.transform_context(context) - context - end - - def self.html_pipeline - @html_pipeline ||= HTML::Pipeline.new(filters) - end - - class << self - %i(call to_document to_html).each do |meth| - define_method(meth) do |text, context| - context = transform_context(context) - - html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend - end - end - end - end - end -end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 7704bf715e4..e06d4dc45f7 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? - Users::UpdateService.new(gl_user).execute! + Users::UpdateService.new(gl_user, user: gl_user).execute! gl_user.block if block_after_save diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 81ecdf43ef9..a99f8e2b5f8 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -65,7 +65,7 @@ module Gitlab # Init new repository # - # storage - project's storage path + # storage - project's storage name # name - project path with namespace # # Ex. @@ -73,7 +73,19 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def add_repository(storage, name) - Gitlab::Git::Repository.create(storage, name, bare: true, symlink_hooks_to: gitlab_shell_hooks_path) + relative_path = name.dup + relative_path << '.git' unless relative_path.end_with?('.git') + + gitaly_migrate(:create_repository) do |is_enabled| + if is_enabled + repository = Gitlab::Git::Repository.new(storage, relative_path, '') + repository.gitaly_repository_client.create_repository + true + else + repo_path = File.join(Gitlab.config.repositories.storages[storage]['path'], relative_path) + Gitlab::Git::Repository.create(repo_path, bare: true, symlink_hooks_to: gitlab_shell_hooks_path) + end + end rescue => err Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}") false diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 36708078136..6857038dba8 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -9,12 +9,28 @@ module Gitlab def uncached_data license_usage_data.merge(system_usage_data) + .merge(features_usage_data) + .merge(components_usage_data) end def to_json(force_refresh: false) data(force_refresh: force_refresh).to_json end + def license_usage_data + usage_data = { + uuid: current_application_settings.uuid, + hostname: Gitlab.config.gitlab.host, + version: Gitlab::VERSION, + active_user_count: User.active.count, + recorded_at: Time.now, + mattermost_enabled: Gitlab.config.mattermost.enabled, + edition: 'CE' + } + + usage_data + end + def system_usage_data { counts: { @@ -54,18 +70,28 @@ module Gitlab } end - def license_usage_data - usage_data = { - uuid: current_application_settings.uuid, - hostname: Gitlab.config.gitlab.host, - version: Gitlab::VERSION, - active_user_count: User.active.count, - recorded_at: Time.now, - mattermost_enabled: Gitlab.config.mattermost.enabled, - edition: 'CE' + def features_usage_data + features_usage_data_ce + end + + def features_usage_data_ce + { + signup: current_application_settings.signup_enabled?, + ldap: Gitlab.config.ldap.enabled, + gravatar: current_application_settings.gravatar_enabled?, + omniauth: Gitlab.config.omniauth.enabled, + reply_by_email: Gitlab::IncomingEmail.enabled?, + container_registry: Gitlab.config.registry.enabled, + gitlab_shared_runners: Gitlab.config.gitlab_ci.shared_runners_enabled } + end - usage_data + def components_usage_data + { + gitlab_pages: { enabled: Gitlab.config.pages.enabled, version: Gitlab::Pages::VERSION }, + git: { version: Gitlab::Git.version }, + database: { adapter: Gitlab::Database.adapter_name, version: Gitlab::Database.version } + } end def services_usage diff --git a/lib/system_check/app/git_version_check.rb b/lib/system_check/app/git_version_check.rb index c388682dfb4..6ee8c8874ec 100644 --- a/lib/system_check/app/git_version_check.rb +++ b/lib/system_check/app/git_version_check.rb @@ -9,7 +9,7 @@ module SystemCheck end def self.current_version - @current_version ||= Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version))) + @current_version ||= Gitlab::VersionInfo.parse(Gitlab::TaskHelpers.run_command(%W(#{Gitlab.config.git.bin_path} --version))) end def check? diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb index fd82f5f8a4a..08a2c495bd4 100644 --- a/lib/system_check/app/ruby_version_check.rb +++ b/lib/system_check/app/ruby_version_check.rb @@ -9,7 +9,7 @@ module SystemCheck end def self.current_version - @current_version ||= Gitlab::VersionInfo.parse(run_command(%w(ruby --version))) + @current_version ||= Gitlab::VersionInfo.parse(Gitlab::TaskHelpers.run_command(%w(ruby --version))) end def check? diff --git a/lib/system_check/incoming_email/imap_authentication_check.rb b/lib/system_check/incoming_email/imap_authentication_check.rb index dee108d987b..e55bea86d3f 100644 --- a/lib/system_check/incoming_email/imap_authentication_check.rb +++ b/lib/system_check/incoming_email/imap_authentication_check.rb @@ -4,22 +4,17 @@ module SystemCheck set_name 'IMAP server credentials are correct?' def check? - if mailbox_config - begin - imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) - imap.starttls if config[:start_tls] - imap.login(config[:email], config[:password]) - connected = true - rescue - connected = false - end + if config + try_connect_imap + else + @error = "#{mail_room_config_path} does not have mailboxes setup" + false end - - connected end def show_error try_fixing_it( + "An error occurred: #{@error.class}: #{@error.message}", 'Check that the information in config/gitlab.yml is correct' ) for_more_information( @@ -30,15 +25,31 @@ module SystemCheck private - def mailbox_config - return @config if @config + def try_connect_imap + imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) + imap.starttls if config[:start_tls] + imap.login(config[:email], config[:password]) + true + rescue => error + @error = error + false + end + + def config + @config ||= load_config + end + + def mail_room_config_path + @mail_room_config_path ||= + Rails.root.join('config', 'mail_room.yml').to_s + end - config_path = Rails.root.join('config', 'mail_room.yml').to_s - erb = ERB.new(File.read(config_path)) - erb.filename = config_path + def load_config + erb = ERB.new(File.read(mail_room_config_path)) + erb.filename = mail_room_config_path config_file = YAML.load(erb.result) - @config = config_file[:mailboxes]&.first + config_file.dig(:mailboxes, 0) end end end diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake index 7ccda04a35f..3eade7bf553 100644 --- a/lib/tasks/gitlab/dev.rake +++ b/lib/tasks/gitlab/dev.rake @@ -13,7 +13,10 @@ namespace :gitlab do args end - if Gitlab::EeCompatCheck.new(opts || {}).check + if File.basename(Rails.root) == 'gitlab-ee' + puts "Skipping EE projects" + exit 0 + elsif Gitlab::EeCompatCheck.new(opts || {}).check exit 0 else exit 1 diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 42825f29e32..0e6aed32c52 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -79,7 +79,7 @@ namespace :gitlab do if File.exist?(path_to_repo) print '-' else - if Gitlab::Shell.new.add_repository(project.repository_storage_path, + if Gitlab::Shell.new.add_repository(project.repository_storage, project.disk_path) print '.' else diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake new file mode 100644 index 00000000000..e05be4a3405 --- /dev/null +++ b/lib/tasks/gitlab/storage.rake @@ -0,0 +1,85 @@ +namespace :gitlab do + namespace :storage do + desc 'GitLab | Storage | Migrate existing projects to Hashed Storage' + task migrate_to_hashed: :environment do + legacy_projects_count = Project.with_legacy_storage.count + + if legacy_projects_count == 0 + puts 'There are no projects using legacy storage. Nothing to do!' + + next + end + + print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}" + + project_id_batches do |start, finish| + StorageMigratorWorker.perform_async(start, finish) + + print '.' + end + + puts ' Done!' + end + + desc 'Gitlab | Storage | Summary of existing projects using Legacy Storage' + task legacy_projects: :environment do + projects_summary(Project.with_legacy_storage) + end + + desc 'Gitlab | Storage | List existing projects using Legacy Storage' + task list_legacy_projects: :environment do + projects_list(Project.with_legacy_storage) + end + + desc 'Gitlab | Storage | Summary of existing projects using Hashed Storage' + task hashed_projects: :environment do + projects_summary(Project.with_hashed_storage) + end + + desc 'Gitlab | Storage | List existing projects using Hashed Storage' + task list_hashed_projects: :environment do + projects_list(Project.with_hashed_storage) + end + + def batch_size + ENV.fetch('BATCH', 200).to_i + end + + def project_id_batches(&block) + Project.with_legacy_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches + ids = relation.pluck(:id) + + yield ids.min, ids.max + end + end + + def projects_summary(relation) + projects_count = relation.count + puts "* Found #{projects_count} projects".color(:green) + + projects_count + end + + def projects_list(relation) + projects_count = projects_summary(relation) + + projects = relation.with_route + limit = ENV.fetch('LIMIT', 500).to_i + + return unless projects_count > 0 + + puts " ! Displaying first #{limit} projects..." if projects_count > limit + + counter = 0 + projects.find_in_batches(batch_size: batch_size) do |batch| + batch.each do |project| + counter += 1 + + puts " - #{project.full_path} (id: #{project.id})".color(:red) + + return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bd3d5b7c839..53e37c53377 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3,12 +3,13 @@ # This file is distributed under the same license as the gitlab package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # +#, fuzzy msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-09-06 08:32+0200\n" -"PO-Revision-Date: 2017-09-06 08:32+0200\n" +"POT-Creation-Date: 2017-09-28 13:28-0400\n" +"PO-Revision-Date: 2017-09-28 13:28-0400\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -30,6 +31,9 @@ msgstr[1] "" msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" +msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" +msgstr "" + msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." msgstr "" @@ -52,6 +56,9 @@ msgid_plural "%d pipelines" msgstr[0] "" msgstr[1] "" +msgid "1st contribution!" +msgstr "" + msgid "A collection of graphs regarding Continuous Integration" msgstr "" @@ -94,7 +101,7 @@ msgstr "" msgid "All" msgstr "" -msgid "Appearances" +msgid "Appearance" msgstr "" msgid "Applications" @@ -118,81 +125,132 @@ msgstr "" msgid "Are you sure?" msgstr "" +msgid "Artifacts" +msgstr "" + msgid "Attach a file by drag & drop or %{upload_link}" msgstr "" -msgid "Authentication log" +msgid "Authentication Log" msgstr "" -msgid "Billing" +msgid "Author" msgstr "" -msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly." msgstr "" -msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly." msgstr "" -msgid "BillingPlans|Current plan" +msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly." msgstr "" -msgid "BillingPlans|Customer Support" +msgid "AutoDevOps|Auto DevOps (Beta)" msgstr "" -msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration." msgstr "" -msgid "BillingPlans|Manage plan" +msgid "AutoDevOps|Auto DevOps documentation" msgstr "" -msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgid "AutoDevOps|Enable in settings" msgstr "" -msgid "BillingPlans|See all %{plan_name} features" +msgid "AutoDevOps|Learn more in the %{link_to_documentation}" msgstr "" -msgid "BillingPlans|This group uses the plan associated with its parent group." +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "" +msgstr[1] "" + +msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "" -msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgid "BranchSwitcherPlaceholder|Search branches" msgstr "" -msgid "BillingPlans|Upgrade" +msgid "BranchSwitcherTitle|Switch branch" msgstr "" -msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgid "Branches" msgstr "" -msgid "BillingPlans|frequently asked questions" +msgid "Branches|Cant find HEAD commit for this branch" msgstr "" -msgid "BillingPlans|monthly" +msgid "Branches|Compare" msgstr "" -msgid "BillingPlans|paid annually at %{price_per_year}" +msgid "Branches|Delete all branches that are merged into '%{default_branch}'" msgstr "" -msgid "BillingPlans|per user" +msgid "Branches|Delete branch" msgstr "" -msgid "Billinglans|Downgrade" +msgid "Branches|Delete merged branches" msgstr "" -msgid "Branch" -msgid_plural "Branches" -msgstr[0] "" -msgstr[1] "" +msgid "Branches|Delete protected branch" +msgstr "" -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}" +msgid "Branches|Delete protected branch '%{branch_name}'?" msgstr "" -msgid "BranchSwitcherPlaceholder|Search branches" +msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?" msgstr "" -msgid "BranchSwitcherTitle|Switch branch" +msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?" msgstr "" -msgid "Branches" +msgid "Branches|Filter by branch name" +msgstr "" + +msgid "Branches|Merged into %{default_branch}" +msgstr "" + +msgid "Branches|New branch" +msgstr "" + +msgid "Branches|No branches to show" +msgstr "" + +msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered." +msgstr "" + +msgid "Branches|Only a project master or owner can delete a protected branch" +msgstr "" + +msgid "Branches|Protected branches can be managed in %{project_settings_link}" +msgstr "" + +msgid "Branches|Sort by" +msgstr "" + +msgid "Branches|The default branch cannot be deleted" +msgstr "" + +msgid "Branches|This branch hasn’t been merged into %{default_branch}." +msgstr "" + +msgid "Branches|To avoid data loss, consider merging this branch before deleting it." +msgstr "" + +msgid "Branches|To confirm, type %{branch_name_confirmation}:" +msgstr "" + +msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}." +msgstr "" + +msgid "Branches|merged" +msgstr "" + +msgid "Branches|project settings" +msgstr "" + +msgid "Branches|protected" msgstr "" msgid "Browse Directory" @@ -216,6 +274,9 @@ msgstr "" msgid "CI configuration" msgstr "" +msgid "CICD|Jobs" +msgstr "" + msgid "Cancel" msgstr "" @@ -303,6 +364,9 @@ msgstr "" msgid "CiStatus|running" msgstr "" +msgid "Clone repository" +msgstr "" + msgid "Comments" msgstr "" @@ -311,6 +375,9 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit Message" +msgstr "" + msgid "Commit duration in minutes for last 30 commits" msgstr "" @@ -338,18 +405,12 @@ msgstr "" msgid "Compare" msgstr "" -msgid "Container Registry" -msgstr "" - msgid "Contribution guide" msgstr "" msgid "Contributors" msgstr "" -msgid "Copy SSH public key to clipboard" -msgstr "" - msgid "Copy URL to clipboard" msgstr "" @@ -425,6 +486,12 @@ msgstr "" msgid "CycleAnalyticsStage|Test" msgstr "" +msgid "DashboardProjects|All" +msgstr "" + +msgid "DashboardProjects|Personal" +msgstr "" + msgid "Define a custom pattern with cron syntax" msgstr "" @@ -517,6 +584,9 @@ msgstr "" msgid "Every week (Sundays at 4:00am)" msgstr "" +msgid "Explore projects" +msgstr "" + msgid "Failed to change the owner" msgstr "" @@ -549,6 +619,9 @@ msgstr[1] "" msgid "ForkedFromProjectPath|Forked from" msgstr "" +msgid "Format" +msgstr "" + msgid "From issue creation until deploy to production" msgstr "" @@ -558,9 +631,6 @@ msgstr "" msgid "GPG Keys" msgstr "" -msgid "Geo Nodes" -msgstr "" - msgid "Git storage health information has been reset" msgstr "" @@ -573,7 +643,28 @@ msgstr "" msgid "GoToYourFork|Fork" msgstr "" -msgid "Group overview" +msgid "GroupSettings|Prevent sharing a project within %{group} with other groups" +msgstr "" + +msgid "GroupSettings|Share with group lock" +msgstr "" + +msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup." +msgstr "" + +msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}." +msgstr "" + +msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}." +msgstr "" + +msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually." +msgstr "" + +msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group" +msgstr "" + +msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}" msgstr "" msgid "Health Check" @@ -594,10 +685,7 @@ msgstr "" msgid "HealthCheck|Unhealthy" msgstr "" -msgid "Home" -msgstr "" - -msgid "Hooks" +msgid "History" msgstr "" msgid "Housekeeping successfully started" @@ -618,6 +706,9 @@ msgstr "" msgid "Issue events" msgstr "" +msgid "IssueBoards|Board" +msgstr "" + msgid "Issues" msgstr "" @@ -644,6 +735,15 @@ msgstr "" msgid "Last commit" msgstr "" +msgid "Last edited %{date}" +msgstr "" + +msgid "Last edited by %{name}" +msgstr "" + +msgid "Last updated" +msgstr "" + msgid "LastPushEvent|You pushed to" msgstr "" @@ -662,17 +762,11 @@ msgstr "" msgid "Leave project" msgstr "" -msgid "License" -msgstr "" - msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "" msgstr[1] "" -msgid "Locked Files" -msgstr "" - msgid "Median" msgstr "" @@ -685,6 +779,9 @@ msgstr "" msgid "Merge events" msgstr "" +msgid "Merge request" +msgstr "" + msgid "Messages" msgstr "" @@ -813,6 +910,18 @@ msgstr "" msgid "Owner" msgstr "" +msgid "Pagination|Last »" +msgstr "" + +msgid "Pagination|Next" +msgstr "" + +msgid "Pagination|Prev" +msgstr "" + +msgid "Pagination|« First" +msgstr "" + msgid "Password" msgstr "" @@ -828,9 +937,6 @@ msgstr "" msgid "Pipeline Schedules" msgstr "" -msgid "Pipeline quota" -msgstr "" - msgid "PipelineCharts|Failed:" msgstr "" @@ -918,10 +1024,7 @@ msgstr "" msgid "Preferences" msgstr "" -msgid "Profile Settings" -msgstr "" - -msgid "Project" +msgid "Profile" msgstr "" msgid "Project '%{project_name}' queued for deletion." @@ -954,12 +1057,6 @@ msgstr "" msgid "Project export started. A download link will be sent by email." msgstr "" -msgid "Project home" -msgstr "" - -msgid "Project overview" -msgstr "" - msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -984,22 +1081,22 @@ msgstr "" msgid "ProjectNetworkGraph|Graph" msgstr "" -msgid "Push Rules" +msgid "ProjectsDropdown|Frequently visited" msgstr "" msgid "ProjectsDropdown|Loading projects" msgstr "" -msgid "ProjectsDropdown|Sorry, no projects matched your search" -msgstr "" - msgid "ProjectsDropdown|Projects you visit often will appear here" msgstr "" msgid "ProjectsDropdown|Search your projects" msgstr "" -msgid "ProjectsDropdown|Something went wrong on our end" +msgid "ProjectsDropdown|Something went wrong on our end." +msgstr "" + +msgid "ProjectsDropdown|Sorry, no projects matched your search" msgstr "" msgid "ProjectsDropdown|This feature requires browser localStorage support" @@ -1068,12 +1165,18 @@ msgstr "" msgid "SSH Keys" msgstr "" +msgid "Save changes" +msgstr "" + msgid "Save pipeline schedule" msgstr "" msgid "Schedule a new pipeline" msgstr "" +msgid "Schedules" +msgstr "" + msgid "Scheduling Pipelines" msgstr "" @@ -1113,6 +1216,12 @@ msgstr "" msgid "Settings" msgstr "" +msgid "Show parent pages" +msgstr "" + +msgid "Show parent subgroups" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "" @@ -1121,6 +1230,93 @@ msgstr[1] "" msgid "Snippets" msgstr "" +msgid "SortOptions|Access level, ascending" +msgstr "" + +msgid "SortOptions|Access level, descending" +msgstr "" + +msgid "SortOptions|Created date" +msgstr "" + +msgid "SortOptions|Due date" +msgstr "" + +msgid "SortOptions|Due later" +msgstr "" + +msgid "SortOptions|Due soon" +msgstr "" + +msgid "SortOptions|Label priority" +msgstr "" + +msgid "SortOptions|Largest group" +msgstr "" + +msgid "SortOptions|Largest repository" +msgstr "" + +msgid "SortOptions|Last created" +msgstr "" + +msgid "SortOptions|Last joined" +msgstr "" + +msgid "SortOptions|Last updated" +msgstr "" + +msgid "SortOptions|Least popular" +msgstr "" + +msgid "SortOptions|Milestone" +msgstr "" + +msgid "SortOptions|Milestone due later" +msgstr "" + +msgid "SortOptions|Milestone due soon" +msgstr "" + +msgid "SortOptions|Most popular" +msgstr "" + +msgid "SortOptions|Name" +msgstr "" + +msgid "SortOptions|Name, ascending" +msgstr "" + +msgid "SortOptions|Name, descending" +msgstr "" + +msgid "SortOptions|Oldest created" +msgstr "" + +msgid "SortOptions|Oldest joined" +msgstr "" + +msgid "SortOptions|Oldest sign in" +msgstr "" + +msgid "SortOptions|Oldest updated" +msgstr "" + +msgid "SortOptions|Popularity" +msgstr "" + +msgid "SortOptions|Priority" +msgstr "" + +msgid "SortOptions|Recent sign in" +msgstr "" + +msgid "SortOptions|Start later" +msgstr "" + +msgid "SortOptions|Start soon" +msgstr "" + msgid "Source code" msgstr "" @@ -1133,6 +1329,9 @@ msgstr "" msgid "StarProject|Star" msgstr "" +msgid "Starred projects" +msgstr "" + msgid "Start a %{new_merge_request} with these changes" msgstr "" @@ -1142,6 +1341,9 @@ msgstr "" msgid "Switch branch/tag" msgstr "" +msgid "System Hooks" +msgstr "" + msgid "Tag" msgid_plural "Tags" msgstr[0] "" @@ -1207,6 +1409,9 @@ msgstr "" msgid "There are problems accessing Git storage: " msgstr "" +msgid "This is the author's first Merge Request to this project." +msgstr "" + msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "" @@ -1382,9 +1587,15 @@ msgstr "" msgid "Use your global notification setting" msgstr "" +msgid "View file @ " +msgstr "" + msgid "View open merge request" msgstr "" +msgid "View replaced file @ " +msgstr "" + msgid "VisibilityLevel|Internal" msgstr "" @@ -1406,6 +1617,105 @@ msgstr "" msgid "Wiki" msgstr "" +msgid "WikiClone|Clone your wiki" +msgstr "" + +msgid "WikiClone|Git Access" +msgstr "" + +msgid "WikiClone|Install Gollum" +msgstr "" + +msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:" +msgstr "" + +msgid "WikiClone|Start Gollum and edit locally" +msgstr "" + +msgid "WikiEmptyPageError|You are not allowed to create wiki pages" +msgstr "" + +msgid "WikiHistoricalPage|This is an old version of this page." +msgstr "" + +msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}." +msgstr "" + +msgid "WikiHistoricalPage|history" +msgstr "" + +msgid "WikiHistoricalPage|most recent version" +msgstr "" + +msgid "WikiMarkdownDocs|More examples are in the %{docs_link}" +msgstr "" + +msgid "WikiMarkdownDocs|documentation" +msgstr "" + +msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}" +msgstr "" + +msgid "WikiNewPagePlaceholder|how-to-setup" +msgstr "" + +msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories." +msgstr "" + +msgid "WikiNewPageTitle|New Wiki Page" +msgstr "" + +msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?" +msgstr "" + +msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs." +msgstr "" + +msgid "WikiPageConflictMessage|the page" +msgstr "" + +msgid "WikiPageCreate|Create %{page_title}" +msgstr "" + +msgid "WikiPageEdit|Update %{page_title}" +msgstr "" + +msgid "WikiPage|Page slug" +msgstr "" + +msgid "WikiPage|Write your content or drag files here..." +msgstr "" + +msgid "Wiki|Create Page" +msgstr "" + +msgid "Wiki|Create page" +msgstr "" + +msgid "Wiki|Edit Page" +msgstr "" + +msgid "Wiki|Empty page" +msgstr "" + +msgid "Wiki|More Pages" +msgstr "" + +msgid "Wiki|New page" +msgstr "" + +msgid "Wiki|Page history" +msgstr "" + +msgid "Wiki|Page version" +msgstr "" + +msgid "Wiki|Pages" +msgstr "" + +msgid "Wiki|Wiki Pages" +msgstr "" + msgid "Withdraw Access Request" msgstr "" @@ -1457,6 +1767,9 @@ msgstr "" msgid "Your name" msgstr "" +msgid "Your projects" +msgstr "" + msgid "day" msgid_plural "days" msgstr[0] "" diff --git a/package.json b/package.json index feae6ca9748..5aa3ce3f757 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "karma": "karma start config/karma.config.js --single-run", "karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run", "karma-start": "karma start config/karma.config.js", + "svg": "node config/svg.config.js", "webpack": "webpack --config config/webpack.config.js", "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, @@ -28,7 +29,7 @@ "css-loader": "^0.28.0", "d3": "^3.5.11", "deckar01-task_list": "^2.0.0", - "document-register-element": "^1.3.0", + "document-register-element": "1.3.0", "dropzone": "^4.2.0", "emoji-unicode-version": "^0.2.1", "eslint-plugin-html": "^2.0.1", @@ -53,6 +54,7 @@ "react-dev-utils": "^0.5.2", "select2": "3.5.2-browserify", "sql.js": "^0.4.0", + "svg4everybody": "2.1.9", "three": "^0.84.0", "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", @@ -78,6 +80,7 @@ "eslint-plugin-import": "^2.2.0", "eslint-plugin-jasmine": "^2.1.0", "eslint-plugin-promise": "^3.5.0", + "gitlab-svgs": "https://gitlab.com/gitlab-org/gitlab-svgs.git", "istanbul": "^0.4.5", "jasmine-core": "^2.6.3", "jasmine-jquery": "^2.1.1", diff --git a/qa/README.md b/qa/README.md index b6b5a76f1d3..e0ebb53a2e9 100644 --- a/qa/README.md +++ b/qa/README.md @@ -16,3 +16,22 @@ against any existing instance. 1. When we release a new version of GitLab, we build a Docker images for it. 1. Along with GitLab Docker Images we also build and publish GitLab QA images. 1. GitLab QA project uses these images to execute integration tests. + +## How can I use it? + +You can use GitLab QA to exercise tests on any live instance! For example, the +follow call would login to the local GitLab instance and run all specs in +`qa/specs/features`: + +``` +GITLAB_USERNAME='root' GITLAB_PASSWORD='5iveL!fe' bin/qa Test::Instance http://localhost +``` + +You can also supply a specific tests to run as another parameter. For example, to +test the EE license specs, you can run: + +``` +EE_LICENSE="<YOUR LICENSE KEY>" GITLAB_USERNAME='root' GITLAB_PASSWORD='5iveL!fe' bin/qa Test::Instance http://localhost qa/ee +``` + +All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables). diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index b01a4e10f93..f4619042e34 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -3,15 +3,10 @@ module QA module Admin class Menu < Page::Base def go_to_license - within_middle_menu { click_link 'License' } - end - - private - - def within_middle_menu - page.within('.nav-control') do - yield - end + link = find_link 'License' + # Click space to scroll this link into the view + link.send_keys(:space) + link.click end end end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 74e53d86266..178c5ea6930 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -11,7 +11,7 @@ module QA end def go_to_admin_area - within_top_menu { click_link 'Admin area' } + within_top_menu { find('.admin-icon').click } end def sign_out diff --git a/scripts/lint-changelog-yaml b/scripts/lint-changelog-yaml new file mode 100755 index 00000000000..cce5f1c7667 --- /dev/null +++ b/scripts/lint-changelog-yaml @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +require 'yaml' + +invalid_changelogs = Dir['changelogs/**/*'].reject do |changelog| + next true if changelog =~ /(archive\.md|unreleased(-ee)?)$/ + next false unless changelog.end_with?('.yml') + + begin + YAML.load_file(changelog) + rescue + end +end + +if invalid_changelogs.any? + puts "Invalid changelogs found!\n" + puts invalid_changelogs.sort + exit 1 +else + puts "All changelogs are valid YAML.\n" + exit 0 +end diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 54c1ef3dfdd..e5242fee32b 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -3,15 +3,19 @@ cd "$(dirname "$0")/.." # Use long options (e.g. --header instead of -H) for curl examples in documentation. -grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ +echo 'Checking for curl short options...' +grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ >/dev/null 2>&1 if [ $? == 0 ] then - echo '✖ ERROR: Short options should not be used in documentation!' >&2 + echo '✖ ERROR: Short options for curl should not be used in documentation! + Use long options (e.g., --header instead of -H):' >&2 + grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ exit 1 fi # Ensure that the CHANGELOG.md does not contain duplicate versions DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d) +echo 'Checking for CHANGELOG.md duplicate entries...' if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ] then echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2 @@ -19,5 +23,15 @@ then exit 1 fi +# Make sure no files in doc/ are executable +EXEC_PERM_COUNT=$(find doc/ app/ -type f -perm 755 | wc -l) +echo 'Checking for executable permissions...' +if [ "${EXEC_PERM_COUNT}" -ne 0 ] +then + echo '✖ ERROR: Executable permissions should not be used in documentation! Use `chmod 644` to the files in question:' >&2 + find doc/ app/ -type f -perm 755 + exit 1 +fi + echo "✔ Linting passed" exit 0 diff --git a/scripts/schema_changed.sh b/scripts/schema_changed.sh new file mode 100644 index 00000000000..5de2b35571d --- /dev/null +++ b/scripts/schema_changed.sh @@ -0,0 +1,10 @@ +function schema_changed() { + if [[ ! -z `git diff --name-only -- db/schema.rb` ]]; then + echo "db/schema.rb after rake db:migrate:reset is different from one in the repository" + exit 1 + else + echo "db/schema.rb after rake db:migrate:reset matches one in the repository" + fi +} + +schema_changed diff --git a/scripts/static-analysis b/scripts/static-analysis index 295b6f132c1..aeefb2bc96f 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -13,7 +13,8 @@ tasks = [ %w[yarn run eslint], %w[bundle exec rubocop --require rubocop-rspec], %w[scripts/lint-conflicts.sh], - %w[bundle exec rake gettext:lint] + %w[bundle exec rake gettext:lint], + %w[scripts/lint-changelog-yaml] ] failed_tasks = tasks.reduce({}) do |failures, task| diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs index 44f832ed3e6..d3a9f5ff4ea 100755 --- a/scripts/trigger-build-docs +++ b/scripts/trigger-build-docs @@ -3,13 +3,6 @@ require 'gitlab' # -# Give the remote branch a different name than the current one -# in order to avoid conflicts -# -@docs_branch = "#{ENV["CI_COMMIT_REF_SLUG"]}-built-from-ce-ee" -GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze - -# # Configure credentials to be used with gitlab gem # Gitlab.configure do |config| @@ -18,6 +11,26 @@ Gitlab.configure do |config| end # +# The remote docs project +# +GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze + +# +# Truncate the remote docs branch name if it's more than 63 characters +# otherwise we hit the filesystem limit and the directory name where +# NGINX serves the site won't match the branch name. +# +def docs_branch + # The maximum string length a file can have on a filesystem (ext4) + # is 63 characters. Let's use something smaller to be 100% sure. + max = 42 + # Prefix the remote branch with 'preview-' in order to avoid + # name conflicts in the rare case the branch name already + # exists in the docs repo and truncate to max length. + "preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max] +end + +# # Dummy way to find out in which repo we are, CE or EE # def ee? @@ -28,18 +41,18 @@ end # Create a remote branch in gitlab-docs # def create_remote_branch - Gitlab.create_branch(GITLAB_DOCS_REPO, @docs_branch, 'master') - puts "Remote branch '#{@docs_branch}' created" + Gitlab.create_branch(GITLAB_DOCS_REPO, docs_branch, 'master') + puts "Remote branch '#{docs_branch}' created" rescue Gitlab::Error::BadRequest - puts "Remote branch '#{@docs_branch}' already exists" + puts "Remote branch '#{docs_branch}' already exists" end # # Remove a remote branch in gitlab-docs # def remove_remote_branch - Gitlab.delete_branch(GITLAB_DOCS_REPO, @docs_branch) - puts "Remote branch '#{@docs_branch}' deleted" + Gitlab.delete_branch(GITLAB_DOCS_REPO, docs_branch) + puts "Remote branch '#{docs_branch}' deleted" end # @@ -50,11 +63,11 @@ def trigger_pipeline param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE' # The review app URL - app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}" + app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}" # Create the pipeline puts "=> Triggering a pipeline..." - pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["DOCS_TRIGGER_TOKEN"], @docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] }) + pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["CI_JOB_TOKEN"], docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] }) puts "=> Pipeline created:" puts "" @@ -77,4 +90,8 @@ when 'deploy' trigger_pipeline when 'cleanup' remove_remote_branch +else + puts "Please provide a valid option: + deploy - Creates the remote branch and triggers a pipeline + cleanup - Deletes the remote branch and stops the Review App" end diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index cc389e554ad..9e9cf4f2c1f 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -10,6 +10,7 @@ describe HealthController do before do allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip]) + stub_storage_settings({}) # Hide the broken storage stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 7493b0a8b35..958d62181a2 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -143,6 +143,16 @@ FactoryGirl.define do end end + trait :wiki_repo do + after(:create) do |project| + raise 'Failed to create wiki repository!' unless project.create_wiki + end + end + + trait :readonly do + repository_read_only true + end + trait :broken_repo do after(:create) do |project| raise "Failed to create repository!" unless project.create_repository diff --git a/spec/factories/user_custom_attributes.rb b/spec/factories/user_custom_attributes.rb new file mode 100644 index 00000000000..278cf290d4f --- /dev/null +++ b/spec/factories/user_custom_attributes.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :user_custom_attribute do + user + sequence(:key) { |n| "key#{n}" } + sequence(:value) { |n| "value#{n}" } + end +end diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb index facb67ae787..8759950e013 100644 --- a/spec/features/dashboard/issues_filter_spec.rb +++ b/spec/features/dashboard/issues_filter_spec.rb @@ -90,17 +90,17 @@ feature 'Dashboard Issues filtering', :js do context 'sorting' do it 'shows sorted issues' do - sorting_by('Oldest updated') + sorting_by('Created date') visit_issues - expect(find('.issues-filters')).to have_content('Oldest updated') + expect(find('.issues-filters')).to have_content('Created date') end it 'keeps sorting issues after visiting Projects Issues page' do - sorting_by('Oldest updated') + sorting_by('Created date') visit project_issues_path(project) - expect(find('.issues-filters')).to have_content('Oldest updated') + expect(find('.issues-filters')).to have_content('Created date') end end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index b4992dd54a1..8204828b5b9 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -112,19 +112,19 @@ feature 'Dashboard Merge Requests' do end it 'shows sorted merge requests' do - sorting_by('Oldest updated') + sorting_by('Created date') visit merge_requests_dashboard_path(assignee_id: current_user.id) - expect(find('.issues-filters')).to have_content('Oldest updated') + expect(find('.issues-filters')).to have_content('Created date') end it 'keeps sorting merge requests after visiting Projects MR page' do - sorting_by('Oldest updated') + sorting_by('Created date') visit project_merge_requests_path(project) - expect(find('.issues-filters')).to have_content('Oldest updated') + expect(find('.issues-filters')).to have_content('Created date') end end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 9a7b8e3ba6b..4da95ccc169 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -50,6 +50,25 @@ feature 'Dashboard Projects' do end end + context 'when on Your projects tab' do + it 'shows all projects by default' do + visit dashboard_projects_path + + expect(page).to have_content(project.name) + end + + it 'shows personal projects on personal projects tab', :js do + project3 = create(:project, namespace: user.namespace) + + visit dashboard_projects_path + + click_link 'Personal' + + expect(page).not_to have_content(project.name) + expect(page).to have_content(project3.name) + end + end + context 'when on Starred projects tab' do it 'shows only starred projects' do user.toggle_star(project2) diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index b72b690110f..925d026ed61 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -40,10 +40,10 @@ describe 'Projects > Issuables > Default sort order' do context 'in the "merge requests / open" tab', js: true do let(:issuable_type) { :merge_request } - it 'is "last created"' do + it 'is "created date"' do visit_merge_requests_with_state(project, 'open') - expect(selected_sort_order).to eq('last created') + expect(selected_sort_order).to eq('created date') expect(first_merge_request).to include(last_created_issuable.title) expect(last_merge_request).to include(first_created_issuable.title) end @@ -76,10 +76,10 @@ describe 'Projects > Issuables > Default sort order' do context 'in the "merge requests / all" tab', js: true do let(:issuable_type) { :merge_request } - it 'is "last created"' do + it 'is "created date"' do visit_merge_requests_with_state(project, 'all') - expect(find('.issues-other-filters')).to have_content('Last created') + expect(find('.issues-other-filters')).to have_content('Created date') expect(first_merge_request).to include(last_created_issuable.title) expect(last_merge_request).to include(first_created_issuable.title) end @@ -105,10 +105,10 @@ describe 'Projects > Issuables > Default sort order' do context 'in the "issues" tab', js: true do let(:issuable_type) { :issue } - it 'is "last created"' do + it 'is "created date"' do visit_issues project - expect(find('.issues-other-filters')).to have_content('Last created') + expect(find('.issues-other-filters')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end @@ -117,10 +117,10 @@ describe 'Projects > Issuables > Default sort order' do context 'in the "issues / open" tab', js: true do let(:issuable_type) { :issue } - it 'is "last created"' do + it 'is "created date"' do visit_issues_with_state(project, 'open') - expect(find('.issues-other-filters')).to have_content('Last created') + expect(find('.issues-other-filters')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end @@ -141,10 +141,10 @@ describe 'Projects > Issuables > Default sort order' do context 'in the "issues / all" tab', js: true do let(:issuable_type) { :issue } - it 'is "last created"' do + it 'is "created date"' do visit_issues_with_state(project, 'all') - expect(find('.issues-other-filters')).to have_content('Last created') + expect(find('.issues-other-filters')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end @@ -157,26 +157,12 @@ describe 'Projects > Issuables > Default sort order' do visit_issues(project, sort: 'id_desc') end - it 'shows the sort order as last created' do - expect(find('.issues-other-filters')).to have_content('Last created') + it 'shows the sort order as created date' do + expect(find('.issues-other-filters')).to have_content('Created date') expect(first_issue).to include(last_created_issuable.title) expect(last_issue).to include(first_created_issuable.title) end end - - context 'when the sort in the URL is id_asc' do - let(:issuable_type) { :issue } - - before do - visit_issues(project, sort: 'id_asc') - end - - it 'shows the sort order as oldest created' do - expect(find('.issues-other-filters')).to have_content('Oldest created') - expect(first_issue).to include(first_created_issuable.title) - expect(last_issue).to include(last_created_issuable.title) - end - end end def selected_sort_order diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index b2229b44f99..a89dcdf41dc 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -405,7 +405,7 @@ feature 'Issues > Labels bulk assignment' do end def update_issues - click_button 'Update all' + find('.update-selected-issues').trigger('click') wait_for_requests end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 3ea6e1c8863..630d6a10c9c 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -405,20 +405,18 @@ describe 'Filter issues', js: true do end context 'sorting' do - it 'sorts by oldest updated' do - create(:issue, + it 'sorts by created date' do + new_issue = create(:issue, title: '3 days ago', project: project, author: user, - created_at: 3.days.ago, - updated_at: 3.days.ago) + created_at: 3.days.ago) - old_issue = create(:issue, + create(:issue, title: '5 days ago', project: project, author: user, - created_at: 5.days.ago, - updated_at: 5.days.ago) + created_at: 5.days.ago) input_filtered_search('days ago') @@ -427,10 +425,10 @@ describe 'Filter issues', js: true do sort_toggle = find('.filtered-search-wrapper .dropdown-toggle') sort_toggle.click - find('.filtered-search-wrapper .dropdown-menu li a', text: 'Oldest updated').click + find('.filtered-search-wrapper .dropdown-menu li a', text: 'Created date').click wait_for_requests - expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(new_issue.title) end end end diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb index 5eeecaeda47..447281ed19d 100644 --- a/spec/features/issues/filtered_search/recent_searches_spec.rb +++ b/spec/features/issues/filtered_search/recent_searches_spec.rb @@ -104,6 +104,6 @@ describe 'Recent searches', js: true do set_recent_searches(project_1_local_storage_key, 'fail') visit project_issues_path(project_1) - expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches') + expect(find('.flash-alert')).to have_text('An error occurred while parsing recent searches') end end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index bcc6e9bab0f..1f57c110c11 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -118,7 +118,7 @@ feature 'Multiple issue updating from issues#index', :js do end def click_update_issues_button - find('.update-selected-issues').click + find('.update-selected-issues').trigger('click') wait_for_requests end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 5c284a1fe5f..b4222edbcd0 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -131,6 +131,14 @@ describe 'Issues' do end describe 'Issue info' do + it 'links to current issue in breadcrubs' do + issue = create(:issue, project: project) + + visit project_issue_path(project, issue) + + expect(find('.breadcrumbs-sub-title a')[:href]).to end_with(issue_path(issue)) + end + it 'excludes award_emoji from comment count' do issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar') create(:award_emoji, awardable: issue) @@ -190,19 +198,12 @@ describe 'Issues' do let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') } it 'sorts by newest' do - visit project_issues_path(project, sort: sort_value_recently_created) + visit project_issues_path(project, sort: sort_value_created_date) expect(first_issue).to include('foo') expect(last_issue).to include('baz') end - it 'sorts by oldest' do - visit project_issues_path(project, sort: sort_value_oldest_created) - - expect(first_issue).to include('baz') - expect(last_issue).to include('foo') - end - it 'sorts by most recently updated' do baz.updated_at = Time.now + 100 baz.save @@ -211,36 +212,22 @@ describe 'Issues' do expect(first_issue).to include('baz') end - it 'sorts by least recently updated' do - baz.updated_at = Time.now - 100 - baz.save - visit project_issues_path(project, sort: sort_value_oldest_updated) - - expect(first_issue).to include('baz') - end - describe 'sorting by due date' do before do foo.update(due_date: 1.day.from_now) bar.update(due_date: 6.days.from_now) end - it 'sorts by recently due date' do - visit project_issues_path(project, sort: sort_value_due_date_soon) + it 'sorts by due date' do + visit project_issues_path(project, sort: sort_value_due_date) expect(first_issue).to include('foo') end - it 'sorts by least recently due date' do - visit project_issues_path(project, sort: sort_value_due_date_later) - - expect(first_issue).to include('bar') - end - - it 'sorts by least recently due date by excluding nil due dates' do + it 'sorts by due date by excluding nil due dates' do bar.update(due_date: nil) - visit project_issues_path(project, sort: sort_value_due_date_later) + visit project_issues_path(project, sort: sort_value_due_date) expect(first_issue).to include('foo') end @@ -339,19 +326,12 @@ describe 'Issues' do bar.save end - it 'sorts by recently due milestone' do - visit project_issues_path(project, sort: sort_value_milestone_soon) + it 'sorts by milestone' do + visit project_issues_path(project, sort: sort_value_milestone) expect(first_issue).to include('foo') expect(last_issue).to include('baz') end - - it 'sorts by least recently due milestone' do - visit project_issues_path(project, sort: sort_value_milestone_later) - - expect(first_issue).to include('bar') - expect(last_issue).to include('baz') - end end describe 'combine filter and sort' do @@ -365,13 +345,11 @@ describe 'Issues' do end it 'sorts with a filter applied' do - visit project_issues_path(project, - sort: sort_value_oldest_created, - assignee_id: user2.id) + visit project_issues_path(project, sort: sort_value_created_date, assignee_id: user2.id) - expect(first_issue).to include('bar') - expect(last_issue).to include('foo') - expect(page).not_to have_content 'baz' + expect(first_issue).to include('foo') + expect(last_issue).to include('bar') + expect(page).not_to have_content('baz') end end end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index b51ae0890e4..16703bc1c01 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -277,9 +277,9 @@ describe 'Filter merge requests' do expect_mr_list_count(2) - click_button 'Last created' + click_button 'Created date' page.within '.dropdown-menu-sort' do - click_link 'Oldest created' + click_link 'Priority' end wait_for_requests diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb index e6dc284cba7..9cb8a357309 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -127,7 +127,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end def click_update_merge_requests_button - find('.update-selected-issues').click + find('.update-selected-issues').trigger('click') wait_for_requests end end diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index 20008b4e7f9..416a0f78a45 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -52,21 +52,13 @@ describe 'Projects > Merge requests > User lists merge requests' do end it 'sorts by newest' do - visit_merge_requests(project, sort: sort_value_recently_created) + visit_merge_requests(project, sort: sort_value_created_date) expect(first_merge_request).to include('fix') expect(last_merge_request).to include('merge-test') expect(count_merge_requests).to eq(3) end - it 'sorts by oldest' do - visit_merge_requests(project, sort: sort_value_oldest_created) - - expect(first_merge_request).to include('merge-test') - expect(last_merge_request).to include('fix') - expect(count_merge_requests).to eq(3) - end - it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) @@ -74,33 +66,19 @@ describe 'Projects > Merge requests > User lists merge requests' do expect(count_merge_requests).to eq(3) end - it 'sorts by oldest updated' do - visit_merge_requests(project, sort: sort_value_oldest_updated) - - expect(first_merge_request).to include('markdown') - expect(count_merge_requests).to eq(3) - end - - it 'sorts by milestone due soon' do - visit_merge_requests(project, sort: sort_value_milestone_soon) + it 'sorts by milestone' do + visit_merge_requests(project, sort: sort_value_milestone) expect(first_merge_request).to include('fix') expect(count_merge_requests).to eq(3) end - it 'sorts by milestone due later' do - visit_merge_requests(project, sort: sort_value_milestone_later) - - expect(first_merge_request).to include('markdown') - expect(count_merge_requests).to eq(3) - end - - it 'filters on one label and sorts by due soon' do + it 'filters on one label and sorts by due date' do label = create(:label, project: project) create(:label_link, label: label, target: @fix) visit_merge_requests(project, label_name: [label.name], - sort: sort_value_due_date_soon) + sort: sort_value_due_date) expect(first_merge_request).to include('fix') expect(count_merge_requests).to eq(1) @@ -115,9 +93,9 @@ describe 'Projects > Merge requests > User lists merge requests' do create(:label_link, label: label2, target: @fix) end - it 'sorts by due soon' do + it 'sorts by due date' do visit_merge_requests(project, label_name: [label.name, label2.name], - sort: sort_value_due_date_soon) + sort: sort_value_due_date) expect(first_merge_request).to include('fix') expect(count_merge_requests).to eq(1) @@ -127,7 +105,7 @@ describe 'Projects > Merge requests > User lists merge requests' do it 'sorts by due soon' do visit_merge_requests(project, label_name: [label.name, label2.name], assignee_id: user.id, - sort: sort_value_due_date_soon) + sort: sort_value_due_date) expect(first_merge_request).to include('fix') expect(count_merge_requests).to eq(1) @@ -137,7 +115,7 @@ describe 'Projects > Merge requests > User lists merge requests' do visit project_merge_requests_path(project, label_name: [label.name, label2.name], assignee_id: user.id, - sort: sort_value_milestone_soon) + sort: sort_value_milestone) expect(first_merge_request).to include('fix') end diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb new file mode 100644 index 00000000000..f0fe4e00acc --- /dev/null +++ b/spec/features/projects/commit/diff_notes_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +feature 'Commit diff', :js do + include RepoHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + + before do + project.add_master(user) + sign_in user + end + + %w(inline parallel).each do |view| + context "#{view} view" do + before do + visit project_commit_path(project, sample_commit.id, view: view) + end + + it "adds comment to diff" do + diff_line_num = first('.diff-line-num.new') + + diff_line_num.trigger('mouseover') + diff_line_num.find('.js-add-diff-note-button').trigger('click') + + page.within(first('.diff-viewer')) do + find('.js-note-text').set 'test comment' + + click_button 'Comment' + + expect(page).to have_content('test comment') + end + end + end + end +end diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb index a6f52c9ef58..c1307ab640f 100644 --- a/spec/features/projects/diffs/diff_show_spec.rb +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -62,13 +62,43 @@ feature 'Diff file viewer', :js do end context 'Image file' do - before do - visit_commit('2f63565e7aac07bcdadb654e253078b727143ec4') + context 'Replaced' do + before do + visit_commit('2f63565e7aac07bcdadb654e253078b727143ec4') + end + + it 'shows a rendered image' do + within('.diff-file[id="e986451b8f7397b617dbb6fffcb5539328c56921"]') do + expect(page).to have_css('img[alt="files/images/6049019_460s.jpg"]') + end + end + + it 'shows view replaced and view file links' do + expect(page.all('.file-actions a').length).to eq 2 + expect(page.all('.file-actions a')[0]).to have_content 'View replaced file @' + expect(page.all('.file-actions a')[1]).to have_content 'View file @' + end + end + + context 'Added' do + before do + visit_commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') + end + + it 'shows view file link' do + expect(page.all('.file-actions a').length).to eq 1 + expect(page.all('.file-actions a')[0]).to have_content 'View file @' + end end - it 'shows a rendered image' do - within('.diff-file[id="e986451b8f7397b617dbb6fffcb5539328c56921"]') do - expect(page).to have_css('img[alt="files/images/6049019_460s.jpg"]') + context 'Deleted' do + before do + visit_commit('7fd7a459706ee87be6f855fd98ce8c552b15529a') + end + + it 'shows view file link' do + expect(page.all('.file-actions a').length).to eq 1 + expect(page.all('.file-actions a')[0]).to have_content 'View file @' end end end diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb new file mode 100644 index 00000000000..e10d29e5eea --- /dev/null +++ b/spec/features/projects/fork_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe 'Project fork' do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + + before do + sign_in user + end + + it 'allows user to fork project' do + visit project_path(project) + + expect(page).not_to have_css('a.disabled', text: 'Fork') + end + + it 'disables fork button when user has exceeded project limit' do + user.projects_limit = 0 + user.save! + + visit project_path(project) + + expect(page).to have_css('a.disabled', text: 'Fork') + end + + context 'master in group' do + before do + group = create(:group) + group.add_master(user) + end + + it 'allows user to fork project to group or to user namespace' do + visit project_path(project) + + expect(page).not_to have_css('a.disabled', text: 'Fork') + + click_link 'Fork' + + expect(page).to have_css('.fork-thumbnail', count: 2) + expect(page).not_to have_css('.fork-thumbnail.disabled') + end + + it 'allows user to fork project to group and not user when exceeded project limit' do + user.projects_limit = 0 + user.save! + + visit project_path(project) + + expect(page).not_to have_css('a.disabled', text: 'Fork') + + click_link 'Fork' + + expect(page).to have_css('.fork-thumbnail', count: 2) + expect(page).to have_css('.fork-thumbnail.disabled') + end + end +end diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb deleted file mode 100644 index c10ec5e2987..00000000000 --- a/spec/features/projects/services/slack_service_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'spec_helper' - -feature 'Projects > Slack service > Setup events' do - let(:user) { create(:user) } - let(:service) { SlackService.new } - let(:project) { create(:project, slack_service: service) } - - background do - service.fields - service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7) - project.team << [user, :master] - sign_in(user) - end - - scenario 'user can filter events by channel' do - visit edit_project_service_path(project, service) - - expect(page.find_field("service_push_channel").value).to have_content '1' - expect(page.find_field("service_issue_channel").value).to have_content '2' - expect(page.find_field("service_merge_request_channel").value).to have_content '3' - expect(page.find_field("service_note_channel").value).to have_content '4' - expect(page.find_field("service_tag_push_channel").value).to have_content '5' - expect(page.find_field("service_pipeline_channel").value).to have_content '6' - expect(page.find_field("service_wiki_page_channel").value).to have_content '7' - end -end diff --git a/spec/features/projects/services/user_activates_asana_spec.rb b/spec/features/projects/services/user_activates_asana_spec.rb new file mode 100644 index 00000000000..db836d2985c --- /dev/null +++ b/spec/features/projects/services/user_activates_asana_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe 'User activates Asana' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Asana') + end + + it 'activates service' do + check('Active') + fill_in('Api key', with: 'verySecret') + fill_in('Restrict to branch', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('Asana activated.') + end +end diff --git a/spec/features/projects/services/user_activates_assembla_spec.rb b/spec/features/projects/services/user_activates_assembla_spec.rb new file mode 100644 index 00000000000..f099b332785 --- /dev/null +++ b/spec/features/projects/services/user_activates_assembla_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'User activates Assembla' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Assembla') + end + + it 'activates service' do + check('Active') + fill_in('Token', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('Assembla activated.') + end +end diff --git a/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb b/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb new file mode 100644 index 00000000000..a00c2e0ad99 --- /dev/null +++ b/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'User activates Atlassian Bamboo CI' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Atlassian Bamboo CI') + end + + it 'activates service' do + check('Active') + fill_in('Bamboo url', with: 'http://bamboo.example.com') + fill_in('Build key', with: 'KEY') + fill_in('Username', with: 'user') + fill_in('Password', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('Atlassian Bamboo CI activated.') + + # Password field should not be filled in. + click_link('Atlassian Bamboo CI') + + expect(find_field('Enter new password').value).to be_nil + end +end diff --git a/spec/features/projects/services/user_activates_emails_on_push_spec.rb b/spec/features/projects/services/user_activates_emails_on_push_spec.rb new file mode 100644 index 00000000000..3769875b29c --- /dev/null +++ b/spec/features/projects/services/user_activates_emails_on_push_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'User activates Emails on push' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Emails on push') + end + + it 'activates service' do + check('Active') + fill_in('Recipients', with: 'qa@company.name') + click_button('Save') + + expect(page).to have_content('Emails on push activated.') + end +end diff --git a/spec/features/projects/services/user_activates_flowdock_spec.rb b/spec/features/projects/services/user_activates_flowdock_spec.rb new file mode 100644 index 00000000000..5298d8acaf5 --- /dev/null +++ b/spec/features/projects/services/user_activates_flowdock_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'User activates Flowdock' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Flowdock') + end + + it 'activates service' do + check('Active') + fill_in('Token', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('Flowdock activated.') + end +end diff --git a/spec/features/projects/services/user_activates_hipchat_spec.rb b/spec/features/projects/services/user_activates_hipchat_spec.rb new file mode 100644 index 00000000000..a9bf16642c7 --- /dev/null +++ b/spec/features/projects/services/user_activates_hipchat_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'User activates HipChat' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('HipChat') + end + + context 'with standart settings' do + it 'activates service' do + check('Active') + fill_in('Room', with: 'gitlab') + fill_in('Token', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('HipChat activated.') + end + end + + context 'with custom settings' do + it 'activates service' do + check('Active') + fill_in('Room', with: 'gitlab_custom') + fill_in('Token', with: 'secretCustom') + fill_in('Server', with: 'https://chat.example.com') + click_button('Save') + + expect(page).to have_content('HipChat activated.') + end + end +end diff --git a/spec/features/projects/services/user_activates_irker_spec.rb b/spec/features/projects/services/user_activates_irker_spec.rb new file mode 100644 index 00000000000..435663c818f --- /dev/null +++ b/spec/features/projects/services/user_activates_irker_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe 'User activates Irker (IRC gateway)' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Irker (IRC gateway)') + end + + it 'activates service' do + check('Active') + check('Colorize messages') + fill_in('Recipients', with: 'irc://chat.freenode.net/#commits') + click_button('Save') + + expect(page).to have_content('Irker (IRC gateway) activated.') + end +end diff --git a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb new file mode 100644 index 00000000000..1048803fde8 --- /dev/null +++ b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'User activates JetBrains TeamCity CI' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('JetBrains TeamCity CI') + end + + it 'activates service' do + check('Active') + fill_in('Teamcity url', with: 'http://teamcity.example.com') + fill_in('Build type', with: 'GitlabTest_Build') + fill_in('Username', with: 'user') + fill_in('Password', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('JetBrains TeamCity CI activated.') + end +end diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb index 65e3a487d4b..0a86292ae6c 100644 --- a/spec/features/projects/services/jira_service_spec.rb +++ b/spec/features/projects/services/user_activates_jira_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Setup Jira service', :js do +describe 'User activates Jira', :js do let(:user) { create(:user) } let(:project) { create(:project) } let(:service) { project.create_jira_service } diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb index 95d5e8b14b9..95d5e8b14b9 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb diff --git a/spec/features/projects/services/user_activates_pivotaltracker_spec.rb b/spec/features/projects/services/user_activates_pivotaltracker_spec.rb new file mode 100644 index 00000000000..d5d109ba48b --- /dev/null +++ b/spec/features/projects/services/user_activates_pivotaltracker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'User activates PivotalTracker' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('PivotalTracker') + end + + it 'activates service' do + check('Active') + fill_in('Token', with: 'verySecret') + click_button('Save') + + expect(page).to have_content('PivotalTracker activated.') + end +end diff --git a/spec/features/projects/services/user_activates_pushover_spec.rb b/spec/features/projects/services/user_activates_pushover_spec.rb new file mode 100644 index 00000000000..9b7e8d62792 --- /dev/null +++ b/spec/features/projects/services/user_activates_pushover_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'User activates Pushover' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Pushover') + end + + it 'activates service' do + check('Active') + fill_in('Api key', with: 'verySecret') + fill_in('User key', with: 'verySecret') + fill_in('Device', with: 'myDevice') + select('High Priority', from: 'Priority') + select('Bike', from: 'Sound') + click_button('Save') + + expect(page).to have_content('Pushover activated.') + end +end diff --git a/spec/features/projects/services/user_activates_slack_notifications_spec.rb b/spec/features/projects/services/user_activates_slack_notifications_spec.rb new file mode 100644 index 00000000000..fae9ebd1bd6 --- /dev/null +++ b/spec/features/projects/services/user_activates_slack_notifications_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe 'User activates Slack notifications' do + let(:user) { create(:user) } + let(:service) { SlackService.new } + let(:project) { create(:project, slack_service: service) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when service is not configured yet' do + before do + visit(project_settings_integrations_path(project)) + + click_link('Slack notifications') + end + + it 'activates service' do + check('Active') + fill_in('Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685') + click_button('Save') + + expect(page).to have_content('Slack notifications activated.') + end + end + + context 'when service is already configured' do + before do + service.fields + service.update_attributes( + push_channel: 1, + issue_channel: 2, + merge_request_channel: 3, + note_channel: 4, + tag_push_channel: 5, + pipeline_channel: 6, + wiki_page_channel: 7) + + visit(edit_project_service_path(project, service)) + end + + it 'filters events by channel' do + expect(page.find_field('service_push_channel').value).to have_content('1') + expect(page.find_field('service_issue_channel').value).to have_content('2') + expect(page.find_field('service_merge_request_channel').value).to have_content('3') + expect(page.find_field('service_note_channel').value).to have_content('4') + expect(page.find_field('service_tag_push_channel').value).to have_content('5') + expect(page.find_field('service_pipeline_channel').value).to have_content('6') + expect(page.find_field('service_wiki_page_channel').value).to have_content('7') + end + end +end diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb index a8baf126269..a8baf126269 100644 --- a/spec/features/projects/services/slack_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb new file mode 100644 index 00000000000..f86591c2633 --- /dev/null +++ b/spec/features/projects/services/user_views_services_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe 'User views services' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + end + + it 'shows the list of available services' do + expect(page).to have_content('Project services') + expect(page).to have_content('Campfire') + expect(page).to have_content('HipChat') + expect(page).to have_content('Assembla') + expect(page).to have_content('Pushover') + expect(page).to have_content('Atlassian Bamboo') + expect(page).to have_content('JetBrains TeamCity') + expect(page).to have_content('Asana') + expect(page).to have_content('Irker (IRC gateway)') + end +end diff --git a/spec/features/projects/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb deleted file mode 100644 index bf18c444c3d..00000000000 --- a/spec/features/projects/shortcuts_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -feature 'Project shortcuts' do - let(:project) { create(:project, name: 'Victorialand') } - let(:user) { create(:user) } - - describe 'On a project', js: true do - before do - project.team << [user, :master] - sign_in user - visit project_path(project) - end - - describe 'pressing "i"' do - it 'redirects to new issue page' do - find('body').native.send_key('i') - expect(page).to have_content('Victorialand') - end - end - end -end diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index 3129aad8473..19954313c23 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -20,8 +20,7 @@ describe 'User edits files' do it 'inserts a content of a file', js: true do click_link('.gitignore') find('.js-edit-blob').click - - wait_for_requests + find('.file-editor', match: :first) execute_script("ace.edit('editor').setValue('*.rbca')") @@ -38,8 +37,7 @@ describe 'User edits files' do it 'commits an edited file', js: true do click_link('.gitignore') find('.js-edit-blob').click - - wait_for_requests + find('.file-editor', match: :first) execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) @@ -56,7 +54,7 @@ describe 'User edits files' do click_link('.gitignore') find('.js-edit-blob').click - wait_for_requests + find('.file-editor', match: :first) execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) @@ -67,15 +65,13 @@ describe 'User edits files' do click_link('Changes') - wait_for_requests expect(page).to have_content('*.rbca') end it 'shows the diff of an edited file', js: true do click_link('.gitignore') find('.js-edit-blob').click - - wait_for_requests + find('.file-editor', match: :first) execute_script("ace.edit('editor').setValue('*.rbca')") click_link('Preview changes') @@ -104,7 +100,7 @@ describe 'User edits files' do "A fork of this project has been created that you can make changes in, so you can submit a merge request." ) - wait_for_requests + find('.file-editor', match: :first) execute_script("ace.edit('editor').setValue('*.rbca')") @@ -120,7 +116,7 @@ describe 'User edits files' do click_link('Fork') - wait_for_requests + find('.file-editor', match: :first) execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb new file mode 100644 index 00000000000..fb0d8c766fe --- /dev/null +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe 'User uses shortcuts', :js do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_path(project)) + end + + context 'when navigating to the Overview pages' do + it 'redirects to the details page' do + find('body').native.send_key('g') + find('body').native.send_key('p') + + expect(page).to have_active_navigation('Overview') + expect(page).to have_active_sub_navigation('Details') + end + + it 'redirects to the activity page' do + find('body').native.send_key('g') + find('body').native.send_key('e') + + expect(page).to have_active_navigation('Overview') + expect(page).to have_active_sub_navigation('Activity') + end + end + + context 'when navigating to the Repository pages' do + it 'redirects to the repository files page' do + find('body').native.send_key('g') + find('body').native.send_key('f') + + expect(page).to have_active_navigation('Repository') + expect(page).to have_active_sub_navigation('Files') + end + + it 'redirects to the repository commits page' do + find('body').native.send_key('g') + find('body').native.send_key('c') + + expect(page).to have_active_navigation('Repository') + expect(page).to have_active_sub_navigation('Commits') + end + + it 'redirects to the repository graph page' do + find('body').native.send_key('g') + find('body').native.send_key('n') + + expect(page).to have_active_navigation('Repository') + expect(page).to have_active_sub_navigation('Graph') + end + + it 'redirects to the repository charts page' do + find('body').native.send_key('g') + find('body').native.send_key('d') + + expect(page).to have_active_navigation('Repository') + expect(page).to have_active_sub_navigation('Charts') + end + end + + context 'when navigating to the Issues pages' do + it 'redirects to the issues list page' do + find('body').native.send_key('g') + find('body').native.send_key('i') + + expect(page).to have_active_navigation('Issues') + expect(page).to have_active_sub_navigation('List') + end + + it 'redirects to the new issue page' do + find('body').native.send_key('i') + + expect(page).to have_content(project.title) + end + end + + context 'when navigating to the Merge Requests pages' do + it 'redirects to the merge requests page' do + find('body').native.send_key('g') + find('body').native.send_key('m') + + expect(page).to have_active_navigation('Merge Requests') + end + end + + context 'when navigating to the Snippets pages' do + it 'redirects to the snippets page' do + find('body').native.send_key('g') + find('body').native.send_key('s') + + expect(page).to have_active_navigation('Snippets') + end + end + + context 'when navigating to the Wiki pages' do + it 'redirects to the wiki page' do + find('body').native.send_key('g') + find('body').native.send_key('w') + + expect(page).to have_active_navigation('Wiki') + end + end +end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 1cf14204159..949d90a50ff 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -40,7 +40,7 @@ describe 'User updates wiki page' do expect(current_path).to include('one/two/three-test') expect(find('.wiki-pages')).to have_content('Three') - click_on('Three') + first(:link, text: 'Three').click expect(find('.nav-text')).to have_content('Three') diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index d201d4f6b98..49ba2969ef0 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -34,7 +34,7 @@ describe 'User views a wiki page' do it 'shows the history of a page that has a path', :js do expect(current_path).to include('one/two/three-test') - click_on('Three') + first(:link, text: 'Three').click click_on('Page history') expect(current_path).to include('one/two/three-test') @@ -48,7 +48,7 @@ describe 'User views a wiki page' do expect(current_path).to include('one/two/three-test') expect(find('.wiki-pages')).to have_content('Three') - click_on('Three') + first(:link, text: 'Three').click expect(find('.nav-text')).to have_content('Three') diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index d6a6b8fc7d5..80750c904b5 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -35,15 +35,30 @@ feature 'Master deletes tag' do end context 'when pre-receive hook fails', js: true do - before do - allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags') + context 'when Gitaly operation_user_delete_tag feature is enabled' do + before do + allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag) + .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags') + end + + scenario 'shows the error message' do + delete_first_tag + + expect(page).to have_content('Do not delete tags') + end end - scenario 'shows the error message' do - delete_first_tag + context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do + before do + allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) + .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags') + end + + scenario 'shows the error message' do + delete_first_tag - expect(page).to have_content('Do not delete tags') + expect(page).to have_content('Do not delete tags') + end end end diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb deleted file mode 100644 index 37d66b618af..00000000000 --- a/spec/features/user_callout_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe 'User Callouts', js: true do - let(:user) { create(:user) } - let(:another_user) { create(:user) } - let(:project) { create(:project, path: 'gitlab', name: 'sample') } - - before do - sign_in(user) - project.team << [user, :master] - end - - it 'takes you to the profile preferences when the link is clicked' do - visit dashboard_projects_path - click_link 'Check it out' - expect(current_path).to eq profile_preferences_path - end - - it 'does not show when cookie is set' do - visit dashboard_projects_path - - within('.user-callout') do - find('.close').trigger('click') - end - - visit dashboard_projects_path - - expect(page).not_to have_selector('.user-callout') - end - - describe 'user callout should appear in two routes' do - it 'shows up on the user profile' do - visit user_path(user) - expect(find('.user-callout')).to have_content 'Customize your experience' - end - - it 'shows up on the dashboard projects' do - visit dashboard_projects_path - expect(find('.user-callout')).to have_content 'Customize your experience' - end - end - - it 'hides the user callout when click on the dismiss icon' do - visit user_path(user) - within('.user-callout') do - find('.close').click - end - expect(page).not_to have_selector('.user-callout') - end - - it 'does not show callout on another users profile' do - visit user_path(another_user) - expect(page).not_to have_selector('.user-callout') - end -end diff --git a/spec/finders/fork_projects_finder_spec.rb b/spec/finders/fork_projects_finder_spec.rb new file mode 100644 index 00000000000..f0cef7ea406 --- /dev/null +++ b/spec/finders/fork_projects_finder_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe ForkProjectsFinder do + let(:source_project) { create(:project, :empty_repo) } + let(:private_fork) { create(:project, :private, :empty_repo, name: 'A') } + let(:internal_fork) { create(:project, :internal, :empty_repo, name: 'B') } + let(:public_fork) { create(:project, :public, :empty_repo, name: 'C') } + + let(:non_member) { create(:user) } + let(:private_fork_member) { create(:user) } + + before do + private_fork.add_developer(private_fork_member) + + source_project.forks << private_fork + source_project.forks << internal_fork + source_project.forks << public_fork + end + + describe '#execute' do + let(:finder) { described_class.new(source_project, params: {}, current_user: current_user) } + + subject { finder.execute } + + describe 'without a user' do + let(:current_user) { nil } + + it { is_expected.to eq([public_fork]) } + end + + describe 'with a user' do + let(:current_user) { non_member } + + it { is_expected.to eq([public_fork, internal_fork]) } + end + + describe 'with a member' do + let(:current_user) { private_fork_member } + + it { is_expected.to eq([public_fork, internal_fork, private_fork]) } + end + end +end diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index 1bab6d64388..4249c52c481 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -56,6 +56,15 @@ describe UsersFinder do expect(users.map(&:username)).not_to include([filtered_user_before.username, filtered_user_after.username]) end + + it 'does not filter by custom attributes' do + users = described_class.new( + user, + custom_attributes: { foo: 'bar' } + ).execute + + expect(users).to contain_exactly(user, user1, user2, omniauth_user) + end end context 'with an admin user' do @@ -72,6 +81,19 @@ describe UsersFinder do expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user) end + + it 'filters by custom attributes' do + create :user_custom_attribute, user: user1, key: 'foo', value: 'foo' + create :user_custom_attribute, user: user1, key: 'bar', value: 'bar' + create :user_custom_attribute, user: user2, key: 'foo', value: 'foo' + + users = described_class.new( + admin, + custom_attributes: { foo: 'foo', bar: 'bar' } + ).execute + + expect(users).to contain_exactly(user1) + end end end end diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/detail.json b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json index b7b2535c204..88a3cad62f6 100644 --- a/spec/fixtures/api/schemas/public_api/v4/commit/detail.json +++ b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json @@ -5,11 +5,18 @@ { "required" : [ "stats", - "status" + "status", + "last_pipeline" ], "properties": { "stats": { "$ref": "../commit_stats.json" }, - "status": { "type": ["string", "null"] } + "status": { "type": ["string", "null"] }, + "last_pipeline": { + "oneOf": [ + { "type": "null" }, + { "$ref": "../pipeline/basic.json" } + ] + } } } ] diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json new file mode 100644 index 00000000000..0d127dc5297 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "required" : [ + "id", + "sha", + "ref", + "status" + ], + "properties" : { + "id": { "type": "integer" }, + "sha": { "type": "string" }, + "ref": { "type": "string" }, + "status": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index 4632c679972..f44e7ef6843 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -26,12 +26,13 @@ describe AvatarsHelper do subject { helper.user_avatar_without_link(options) } it 'displays user avatar' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: 'avatar s16 has-tooltip lazy', + is_expected.to eq tag( + :img, alt: "#{user.name}'s avatar", - title: user.name, - data: { container: 'body', src: avatar_icon(user, 16) } + src: avatar_icon(user, 16), + data: { container: 'body' }, + class: 'avatar s16 has-tooltip', + title: user.name ) end @@ -39,12 +40,13 @@ describe AvatarsHelper do let(:options) { { user: user, css_class: '.cat-pics' } } it 'uses provided css_class' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: "avatar s16 #{options[:css_class]} has-tooltip lazy", + is_expected.to eq tag( + :img, alt: "#{user.name}'s avatar", - title: user.name, - data: { container: 'body', src: avatar_icon(user, 16) } + src: avatar_icon(user, 16), + data: { container: 'body' }, + class: "avatar s16 #{options[:css_class]} has-tooltip", + title: user.name ) end end @@ -53,12 +55,13 @@ describe AvatarsHelper do let(:options) { { user: user, size: 99 } } it 'uses provided size' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: "avatar s#{options[:size]} has-tooltip lazy", + is_expected.to eq tag( + :img, alt: "#{user.name}'s avatar", - title: user.name, - data: { container: 'body', src: avatar_icon(user, options[:size]) } + src: avatar_icon(user, options[:size]), + data: { container: 'body' }, + class: "avatar s#{options[:size]} has-tooltip", + title: user.name ) end end @@ -67,12 +70,28 @@ describe AvatarsHelper do let(:options) { { user: user, url: '/over/the/rainbow.png' } } it 'uses provided url' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: 'avatar s16 has-tooltip lazy', + is_expected.to eq tag( + :img, alt: "#{user.name}'s avatar", - title: user.name, - data: { container: 'body', src: options[:url] } + src: options[:url], + data: { container: 'body' }, + class: "avatar s16 has-tooltip", + title: user.name + ) + end + end + + context 'with lazy parameter' do + let(:options) { { user: user, lazy: true } } + + it 'adds `lazy` class to class list, sets `data-src` with avatar URL and `src` with placeholder image' do + is_expected.to eq tag( + :img, + alt: "#{user.name}'s avatar", + src: LazyImageTagHelper.placeholder_image, + data: { container: 'body', src: avatar_icon(user, 16) }, + class: "avatar s16 has-tooltip lazy", + title: user.name ) end end @@ -82,12 +101,13 @@ describe AvatarsHelper do let(:options) { { user: user, has_tooltip: true } } it 'adds has-tooltip' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: 'avatar s16 has-tooltip lazy', + is_expected.to eq tag( + :img, alt: "#{user.name}'s avatar", - title: user.name, - data: { container: 'body', src: avatar_icon(user, 16) } + src: avatar_icon(user, 16), + data: { container: 'body' }, + class: "avatar s16 has-tooltip", + title: user.name ) end end @@ -96,12 +116,12 @@ describe AvatarsHelper do let(:options) { { user: user, has_tooltip: false } } it 'does not add has-tooltip or data container' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: 'avatar s16 lazy', + is_expected.to eq tag( + :img, alt: "#{user.name}'s avatar", - title: user.name, - data: { src: avatar_icon(user, 16) } + src: avatar_icon(user, 16), + class: "avatar s16", + title: user.name ) end end @@ -114,23 +134,25 @@ describe AvatarsHelper do let(:options) { { user: user, user_name: 'Tinky Winky' } } it 'prefers user parameter' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: 'avatar s16 has-tooltip lazy', + is_expected.to eq tag( + :img, alt: "#{user.name}'s avatar", - title: user.name, - data: { container: 'body', src: avatar_icon(user, 16) } + src: avatar_icon(user, 16), + data: { container: 'body' }, + class: "avatar s16 has-tooltip", + title: user.name ) end end it 'uses user_name and user_email parameter if user is not present' do - is_expected.to eq image_tag( - LazyImageTagHelper.placeholder_image, - class: 'avatar s16 has-tooltip lazy', + is_expected.to eq tag( + :img, alt: "#{options[:user_name]}'s avatar", - title: options[:user_name], - data: { container: 'body', src: avatar_icon(options[:user_email], 16) } + src: avatar_icon(options[:user_email], 16), + data: { container: 'body' }, + class: "avatar s16 has-tooltip", + title: options[:user_name] ) end end diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 91c8faea7fd..3d79dac284f 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -16,6 +16,25 @@ describe IconsHelper do end end + describe 'sprite_icon' do + icon_name = 'clock' + + it 'returns svg icon html' do + expect(sprite_icon(icon_name).to_s) + .to eq "<svg><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>" + end + + it 'returns svg icon html + size classes' do + expect(sprite_icon(icon_name, size: 72).to_s) + .to eq "<svg class=\"s72\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>" + end + + it 'returns svg icon html + size classes + additional class' do + expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s) + .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>" + end + end + describe 'file_type_icon_class' do it 'returns folder class' do expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder' diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index a76c75e0c08..7ded95d01af 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -420,22 +420,26 @@ describe ProjectsHelper do end end - describe '#has_projects_or_name?' do + describe '#show_projects' do let(:projects) do create(:project) Project.all end it 'returns true when there are projects' do - expect(helper.has_projects_or_name?(projects, {})).to eq(true) + expect(helper.show_projects?(projects, {})).to eq(true) end it 'returns true when there are no projects but a name is given' do - expect(helper.has_projects_or_name?(Project.none, name: 'foo')).to eq(true) + expect(helper.show_projects?(Project.none, name: 'foo')).to eq(true) + end + + it 'returns true when there are no projects but personal is present' do + expect(helper.show_projects?(Project.none, personal: 'true')).to eq(true) end it 'returns false when there are no projects and there is no name' do - expect(helper.has_projects_or_name?(Project.none, {})).to eq(false) + expect(helper.show_projects?(Project.none, {})).to eq(false) end end diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index 11f2a950678..c3e67550f05 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -117,7 +117,7 @@ describe('iPython notebook renderer', () => { it('shows error message', () => { expect( document.querySelector('.md').textContent.trim(), - ).toBe('An error occured whilst parsing the file.'); + ).toBe('An error occurred whilst parsing the file.'); }); }); @@ -153,7 +153,7 @@ describe('iPython notebook renderer', () => { it('shows error message', () => { expect( document.querySelector('.md').textContent.trim(), - ).toBe('An error occured whilst loading the file. Please try again later.'); + ).toBe('An error occurred whilst loading the file. Please try again later.'); }); }); }); diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js index bbeaf95e68d..51bf3086627 100644 --- a/spec/javascripts/blob/pdf/index_spec.js +++ b/spec/javascripts/blob/pdf/index_spec.js @@ -76,7 +76,7 @@ describe('PDF renderer', () => { it('shows error message', () => { expect( document.querySelector('.md').textContent.trim(), - ).toBe('An error occured whilst loading the file. Please try again later.'); + ).toBe('An error occurred whilst loading the file. Please try again later.'); }); }); }); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 35149611095..d5b0f23e7b7 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -289,4 +289,18 @@ describe('Build', () => { }); }); }); + + describe('getBuildTrace', () => { + it('should request build trace with state parameter', (done) => { + spyOn(jQuery, 'ajax').and.callThrough(); + new Build(); + + setTimeout(() => { + expect(jQuery.ajax).toHaveBeenCalledWith( + { url: `${BUILD_URL}/trace.json`, data: { state: '' } }, + ); + done(); + }, 0); + }); + }); }); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 454f187ccbc..9fc047b1f5e 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -29,6 +29,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -65,6 +67,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -117,6 +121,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -139,6 +145,8 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', autoDevopsHelpPath: 'foo', }, }).$mount(); diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js index 2fb9eb0ca85..13e9fe00a00 100644 --- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js +++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import limitWarningComp from '~/cycle_analytics/components/limit_warning_component'; +import limitWarningComp from '~/cycle_analytics/components/limit_warning_component.vue'; Vue.use(Translate); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index fdaea5c0b0c..7e62d356bd2 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -14,6 +14,10 @@ describe('Environments Folder View', () => { window.history.pushState({}, null, 'environments/folders/build'); }); + afterEach(() => { + window.history.pushState({}, null, '/'); + }); + let component; describe('successfull request', () => { diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js deleted file mode 100644 index 114d282e48a..00000000000 --- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js +++ /dev/null @@ -1,219 +0,0 @@ -import Cookies from 'js-cookie'; -import { - getCookieName, - getSelector, - showPopover, - hidePopover, - dismiss, - mouseleave, - mouseenter, - setupDismissButton, -} from '~/feature_highlight/feature_highlight_helper'; - -describe('feature highlight helper', () => { - describe('getCookieName', () => { - it('returns `feature-highlighted-` prefix', () => { - const cookieId = 'cookieId'; - expect(getCookieName(cookieId)).toEqual(`feature-highlighted-${cookieId}`); - }); - }); - - describe('getSelector', () => { - it('returns js-feature-highlight selector', () => { - const highlightId = 'highlightId'; - expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`); - }); - }); - - describe('showPopover', () => { - it('returns true when popover is shown', () => { - const context = { - hasClass: () => false, - popover: () => {}, - addClass: () => {}, - }; - - expect(showPopover.call(context)).toEqual(true); - }); - - it('returns false when popover is already shown', () => { - const context = { - hasClass: () => true, - }; - - expect(showPopover.call(context)).toEqual(false); - }); - - it('shows popover', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - addClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('show'); - done(); - }); - - showPopover.call(context); - }); - - it('adds disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - addClass: () => {}, - }; - - spyOn(context, 'addClass').and.callFake((classNames) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - done(); - }); - - showPopover.call(context); - }); - }); - - describe('hidePopover', () => { - it('returns true when popover is hidden', () => { - const context = { - hasClass: () => true, - popover: () => {}, - removeClass: () => {}, - }; - - expect(hidePopover.call(context)).toEqual(true); - }); - - it('returns false when popover is already hidden', () => { - const context = { - hasClass: () => false, - }; - - expect(hidePopover.call(context)).toEqual(false); - }); - - it('hides popover', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - removeClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('hide'); - done(); - }); - - hidePopover.call(context); - }); - - it('removes disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - removeClass: () => {}, - }; - - spyOn(context, 'removeClass').and.callFake((classNames) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - done(); - }); - - hidePopover.call(context); - }); - }); - - describe('dismiss', () => { - const context = { - hide: () => {}, - }; - - beforeEach(() => { - spyOn(Cookies, 'set').and.callFake(() => {}); - spyOn(hidePopover, 'call').and.callFake(() => {}); - spyOn(context, 'hide').and.callFake(() => {}); - dismiss.call(context); - }); - - it('sets cookie to true', () => { - expect(Cookies.set).toHaveBeenCalled(); - }); - - it('calls hide popover', () => { - expect(hidePopover.call).toHaveBeenCalled(); - }); - - it('calls hide', () => { - expect(context.hide).toHaveBeenCalled(); - }); - }); - - describe('mouseleave', () => { - it('calls hide popover if .popover:hover is false', () => { - const fakeJquery = { - length: 0, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(hidePopover, 'call'); - mouseleave(); - expect(hidePopover.call).toHaveBeenCalled(); - }); - - it('does not call hide popover if .popover:hover is true', () => { - const fakeJquery = { - length: 1, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(hidePopover, 'call'); - mouseleave(); - expect(hidePopover.call).not.toHaveBeenCalled(); - }); - }); - - describe('mouseenter', () => { - const context = {}; - - it('shows popover', () => { - spyOn(showPopover, 'call').and.returnValue(false); - mouseenter.call(context); - expect(showPopover.call).toHaveBeenCalled(); - }); - - it('registers mouseleave event if popover is showed', (done) => { - spyOn(showPopover, 'call').and.returnValue(true); - spyOn($.fn, 'on').and.callFake((eventName) => { - expect(eventName).toEqual('mouseleave'); - done(); - }); - mouseenter.call(context); - }); - - it('does not register mouseleave event if popover is not showed', () => { - spyOn(showPopover, 'call').and.returnValue(false); - const spy = spyOn($.fn, 'on').and.callFake(() => {}); - mouseenter.call(context); - expect(spy).not.toHaveBeenCalled(); - }); - }); - - describe('setupDismissButton', () => { - it('registers click event callback', (done) => { - const context = { - getAttribute: () => 'popoverId', - dataset: { - highlight: 'cookieId', - }, - }; - - spyOn($.fn, 'on').and.callFake((event) => { - expect(event).toEqual('click'); - done(); - }); - setupDismissButton.call(context); - }); - }); -}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js deleted file mode 100644 index 7feb361edec..00000000000 --- a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import domContentLoaded from '~/feature_highlight/feature_highlight_options'; -import bp from '~/breakpoints'; - -describe('feature highlight options', () => { - describe('domContentLoaded', () => { - const highlightOrder = []; - - beforeEach(() => { - // Check for when highlightFeatures is called - spyOn(highlightOrder, 'find').and.callFake(() => {}); - }); - - it('should not call highlightFeatures when breakpoint is xs', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('xs'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).not.toHaveBeenCalled(); - }); - - it('should not call highlightFeatures when breakpoint is sm', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).not.toHaveBeenCalled(); - }); - - it('should not call highlightFeatures when breakpoint is md', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('md'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).not.toHaveBeenCalled(); - }); - - it('should call highlightFeatures when breakpoint is lg', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('lg'); - - domContentLoaded(highlightOrder); - expect(bp.getBreakpointSize).toHaveBeenCalled(); - expect(highlightOrder.find).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js deleted file mode 100644 index 6abe8425ee7..00000000000 --- a/spec/javascripts/feature_highlight/feature_highlight_spec.js +++ /dev/null @@ -1,122 +0,0 @@ -import Cookies from 'js-cookie'; -import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; -import * as featureHighlight from '~/feature_highlight/feature_highlight'; - -describe('feature highlight', () => { - describe('setupFeatureHighlightPopover', () => { - const selector = '.js-feature-highlight[data-highlight=test]'; - beforeEach(() => { - setFixtures(` - <div> - <div class="js-feature-highlight" data-highlight="test" disabled> - Trigger - </div> - </div> - <div class="feature-highlight-popover-content"> - Content - <div class="dismiss-feature-highlight"> - Dismiss - </div> - </div> - `); - spyOn(window, 'addEventListener'); - spyOn(window, 'removeEventListener'); - featureHighlight.setupFeatureHighlightPopover('test', 0); - }); - - it('setups popover content', () => { - const $popoverContent = $('.feature-highlight-popover-content'); - const outerHTML = $popoverContent.prop('outerHTML'); - - expect($(selector).data('content')).toEqual(outerHTML); - }); - - it('setups mouseenter', () => { - const showSpy = spyOn(featureHighlightHelper.showPopover, 'call'); - $(selector).trigger('mouseenter'); - - expect(showSpy).toHaveBeenCalled(); - }); - - it('setups debounced mouseleave', (done) => { - const hideSpy = spyOn(featureHighlightHelper.hidePopover, 'call'); - $(selector).trigger('mouseleave'); - - // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce - setTimeout(() => { - expect(hideSpy).toHaveBeenCalled(); - done(); - }, 0); - }); - - it('setups inserted.bs.popover', () => { - $(selector).trigger('mouseenter'); - const popoverId = $(selector).attr('aria-describedby'); - const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); - - $(`#${popoverId} .dismiss-feature-highlight`).click(); - expect(spyEvent).toHaveBeenTriggered(); - }); - - it('setups show.bs.popover', () => { - $(selector).trigger('show.bs.popover'); - expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - }); - - it('setups hide.bs.popover', () => { - $(selector).trigger('hide.bs.popover'); - expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - }); - - it('removes disabled attribute', () => { - expect($('.js-feature-highlight').is(':disabled')).toEqual(false); - }); - - it('displays popover', () => { - expect($(selector).attr('aria-describedby')).toBeFalsy(); - $(selector).trigger('mouseenter'); - expect($(selector).attr('aria-describedby')).toBeTruthy(); - }); - }); - - describe('shouldHighlightFeature', () => { - it('should return false if element is not found', () => { - spyOn(document, 'querySelector').and.returnValue(null); - spyOn(Cookies, 'get').and.returnValue(null); - - expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); - }); - - it('should return false if previouslyDismissed', () => { - spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); - spyOn(Cookies, 'get').and.returnValue('true'); - - expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); - }); - - it('should return true if element is found and not previouslyDismissed', () => { - spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); - spyOn(Cookies, 'get').and.returnValue(null); - - expect(featureHighlight.shouldHighlightFeature()).toBeTruthy(); - }); - }); - - describe('highlightFeatures', () => { - it('calls setupFeatureHighlightPopover if shouldHighlightFeature returns true', () => { - // Mimic shouldHighlightFeature set to true - const highlightOrder = ['issue-boards']; - spyOn(highlightOrder, 'find').and.returnValue(highlightOrder[0]); - - expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(true); - }); - - it('does not call setupFeatureHighlightPopover if shouldHighlightFeature returns false', () => { - // Mimic shouldHighlightFeature set to false - const highlightOrder = ['issue-boards']; - spyOn(highlightOrder, 'find').and.returnValue(null); - - expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(false); - }); - }); -}); diff --git a/spec/javascripts/fixtures/dashboard.rb b/spec/javascripts/fixtures/dashboard.rb deleted file mode 100644 index 7fa351680c9..00000000000 --- a/spec/javascripts/fixtures/dashboard.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Dashboard::ProjectsController, '(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: 'builds-project') } - - render_views - - before(:all) do - clean_frontend_fixtures('dashboard/') - end - - before do - sign_in(admin) - end - - after do - remove_repository(project) - end - - it 'dashboard/user-callout.html.raw' do |example| - rendered = render_template('shared/_user_callout') - store_frontend_fixture(rendered, example.description) - end - - private - - def render_template(template_file_name) - controller.prepend_view_path(JavaScriptFixturesHelpers::FIXTURE_PATH) - controller.render_to_string(template_file_name, layout: false) - end -end diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml index 418a38a0e2e..97b0c25c923 100644 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -2,6 +2,8 @@ #pipelines-list-vue{ data: { endpoint: 'foo', "css-class" => 'foo', "help-page-path" => 'foo', + "empty-state-svg-path" => 'foo', + "error-state-svg-path" => 'foo', "new-pipeline-path" => 'foo', "can-create-pipeline" => 'true', "all-path" => 'foo', diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index f4b4d7980a4..4f20e31f511 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -73,6 +73,12 @@ describe('Fly out sidebar navigation', () => { ).toBe(0); }); + it('returns 0 if mousePos is empty', () => { + expect( + getHideSubItemsInterval(), + ).toBe(0); + }); + it('returns 0 when mouse above sub-items', () => { showSubLevelItems(el); documentMouseMove({ diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a6ad250bd86..f86f2f260c3 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,398 +1,481 @@ /* eslint-disable promise/catch-or-return */ -import '~/lib/utils/common_utils'; +import * as commonUtils from '~/lib/utils/common_utils'; -(() => { - describe('common_utils', () => { - describe('gl.utils.parseUrl', () => { - it('returns an anchor tag with url', () => { - expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); - }); - it('url is escaped', () => { - // IE11 will return a relative pathname while other browsers will return a full pathname. - // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor - // element will create an absolute url relative to the current execution context. - // The JavaScript test suite is executed at '/' which will lead to an absolute url - // starting with '/'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); - }); +describe('common_utils', () => { + describe('parseUrl', () => { + it('returns an anchor tag with url', () => { + expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); }); + it('url is escaped', () => { + // IE11 will return a relative pathname while other browsers will return a full pathname. + // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor + // element will create an absolute url relative to the current execution context. + // The JavaScript test suite is executed at '/' which will lead to an absolute url + // starting with '/'. + expect(commonUtils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); + }); + }); - describe('gl.utils.parseUrlPathname', () => { - beforeEach(() => { - spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ - pathname: url, - })); - }); - it('returns an absolute url when given an absolute url', () => { - expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); - }); - it('returns an absolute url when given a relative url', () => { - expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); - }); + describe('parseUrlPathname', () => { + it('returns an absolute url when given an absolute url', () => { + expect(commonUtils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); }); - describe('gl.utils.getUrlParamsArray', () => { - it('should return params array', () => { - expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true); - }); + it('returns an absolute url when given a relative url', () => { + expect(commonUtils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); + }); + }); - it('should remove the question mark from the search params', () => { - const paramsArray = gl.utils.getUrlParamsArray(); - expect(paramsArray[0][0] !== '?').toBe(true); - }); + describe('getUrlParamsArray', () => { + it('should return params array', () => { + expect(commonUtils.getUrlParamsArray() instanceof Array).toBe(true); + }); + + it('should remove the question mark from the search params', () => { + const paramsArray = commonUtils.getUrlParamsArray(); + expect(paramsArray[0][0] !== '?').toBe(true); + }); - it('should decode params', () => { - history.pushState('', '', '?label_name%5B%5D=test'); + it('should decode params', () => { + history.pushState('', '', '?label_name%5B%5D=test'); - expect( - gl.utils.getUrlParamsArray()[0], - ).toBe('label_name[]=test'); + expect( + commonUtils.getUrlParamsArray()[0], + ).toBe('label_name[]=test'); - history.pushState('', '', '?'); - }); + history.pushState('', '', '?'); }); + }); - describe('gl.utils.handleLocationHash', () => { - beforeEach(() => { - spyOn(window.document, 'getElementById').and.callThrough(); - }); + describe('handleLocationHash', () => { + beforeEach(() => { + spyOn(window.document, 'getElementById').and.callThrough(); + }); - afterEach(() => { - window.history.pushState({}, null, ''); - }); + afterEach(() => { + window.history.pushState({}, null, ''); + }); - function expectGetElementIdToHaveBeenCalledWith(elementId) { - expect(window.document.getElementById).toHaveBeenCalledWith(elementId); - } + function expectGetElementIdToHaveBeenCalledWith(elementId) { + expect(window.document.getElementById).toHaveBeenCalledWith(elementId); + } - it('decodes hash parameter', () => { - window.history.pushState({}, null, '#random-hash'); - gl.utils.handleLocationHash(); + it('decodes hash parameter', () => { + window.history.pushState({}, null, '#random-hash'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('random-hash'); - expectGetElementIdToHaveBeenCalledWith('user-content-random-hash'); - }); + expectGetElementIdToHaveBeenCalledWith('random-hash'); + expectGetElementIdToHaveBeenCalledWith('user-content-random-hash'); + }); - it('decodes cyrillic hash parameter', () => { - window.history.pushState({}, null, '#definição'); - gl.utils.handleLocationHash(); + it('decodes cyrillic hash parameter', () => { + window.history.pushState({}, null, '#definição'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('definição'); - expectGetElementIdToHaveBeenCalledWith('user-content-definição'); - }); + expectGetElementIdToHaveBeenCalledWith('definição'); + expectGetElementIdToHaveBeenCalledWith('user-content-definição'); + }); - it('decodes encoded cyrillic hash parameter', () => { - window.history.pushState({}, null, '#defini%C3%A7%C3%A3o'); - gl.utils.handleLocationHash(); + it('decodes encoded cyrillic hash parameter', () => { + window.history.pushState({}, null, '#defini%C3%A7%C3%A3o'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('definição'); - expectGetElementIdToHaveBeenCalledWith('user-content-definição'); - }); + expectGetElementIdToHaveBeenCalledWith('definição'); + expectGetElementIdToHaveBeenCalledWith('user-content-definição'); }); - describe('gl.utils.setParamInURL', () => { - afterEach(() => { - window.history.pushState({}, null, ''); - }); + it('scrolls element into view', () => { + document.body.innerHTML += ` + <div id="parent"> + <div style="height: 2000px;"></div> + <div id="test" style="height: 2000px;"></div> + </div> + `; - it('should return the parameter', () => { - window.history.replaceState({}, null, ''); + window.history.pushState({}, null, '#test'); + commonUtils.handleLocationHash(); - expect(gl.utils.setParamInURL('page', 156)).toBe('?page=156'); - expect(gl.utils.setParamInURL('page', '156')).toBe('?page=156'); - }); + expectGetElementIdToHaveBeenCalledWith('test'); + expect(window.scrollY).toBe(document.getElementById('test').offsetTop); - it('should update the existing parameter when its a number', () => { - window.history.pushState({}, null, '?page=15'); + document.getElementById('parent').remove(); + }); - expect(gl.utils.setParamInURL('page', 16)).toBe('?page=16'); - expect(gl.utils.setParamInURL('page', '16')).toBe('?page=16'); - expect(gl.utils.setParamInURL('page', true)).toBe('?page=true'); - }); + it('scrolls user content element into view', () => { + document.body.innerHTML += ` + <div id="parent"> + <div style="height: 2000px;"></div> + <div id="user-content-test" style="height: 2000px;"></div> + </div> + `; - it('should update the existing parameter when its a string', () => { - window.history.pushState({}, null, '?scope=all'); + window.history.pushState({}, null, '#test'); + commonUtils.handleLocationHash(); - expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); - }); + expectGetElementIdToHaveBeenCalledWith('test'); + expectGetElementIdToHaveBeenCalledWith('user-content-test'); + expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop); - it('should update the existing parameter when more than one parameter exists', () => { - window.history.pushState({}, null, '?scope=all&page=15'); + document.getElementById('parent').remove(); + }); - expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); - }); + it('scrolls to element with offset from navbar', () => { + spyOn(window, 'scrollBy').and.callThrough(); + document.body.innerHTML += ` + <div id="parent"> + <div class="navbar-gitlab" style="position: fixed; top: 0; height: 50px;"></div> + <div style="height: 2000px; margin-top: 50px;"></div> + <div id="user-content-test" style="height: 2000px;"></div> + </div> + `; + + window.history.pushState({}, null, '#test'); + commonUtils.handleLocationHash(); + + expectGetElementIdToHaveBeenCalledWith('test'); + expectGetElementIdToHaveBeenCalledWith('user-content-test'); + expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop - 50); + expect(window.scrollBy).toHaveBeenCalledWith(0, -50); + + document.getElementById('parent').remove(); + }); + }); + + describe('setParamInURL', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); - it('should add a new parameter to the end of the existing ones', () => { - window.history.pushState({}, null, '?scope=all'); + it('should return the parameter', () => { + window.history.replaceState({}, null, ''); - expect(gl.utils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); - expect(gl.utils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); - expect(gl.utils.setParamInURL('page', true)).toBe('?scope=all&page=true'); - }); + expect(commonUtils.setParamInURL('page', 156)).toBe('?page=156'); + expect(commonUtils.setParamInURL('page', '156')).toBe('?page=156'); }); - describe('gl.utils.getParameterByName', () => { - beforeEach(() => { - window.history.pushState({}, null, '?scope=all&p=2'); - }); + it('should update the existing parameter when its a number', () => { + window.history.pushState({}, null, '?page=15'); - afterEach(() => { - window.history.replaceState({}, null, null); - }); + expect(commonUtils.setParamInURL('page', 16)).toBe('?page=16'); + expect(commonUtils.setParamInURL('page', '16')).toBe('?page=16'); + expect(commonUtils.setParamInURL('page', true)).toBe('?page=true'); + }); - it('should return valid parameter', () => { - const value = gl.utils.getParameterByName('scope'); - expect(gl.utils.getParameterByName('p')).toEqual('2'); - expect(value).toBe('all'); - }); + it('should update the existing parameter when its a string', () => { + window.history.pushState({}, null, '?scope=all'); - it('should return invalid parameter', () => { - const value = gl.utils.getParameterByName('fakeParameter'); - expect(value).toBe(null); - }); + expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); + }); - it('should return valid paramentes if URL is provided', () => { - let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); - expect(value).toBe('bar'); + it('should update the existing parameter when more than one parameter exists', () => { + window.history.pushState({}, null, '?scope=all&page=15'); - value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); - expect(value).toBe('canchu'); - }); + expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); }); - describe('gl.utils.normalizedHeaders', () => { - it('should upperCase all the header keys to keep them consistent', () => { - const apiHeaders = { - 'X-Something-Workhorse': { workhorse: 'ok' }, - 'x-something-nginx': { nginx: 'ok' }, - }; + it('should add a new parameter to the end of the existing ones', () => { + window.history.pushState({}, null, '?scope=all'); - const normalized = gl.utils.normalizeHeaders(apiHeaders); + expect(commonUtils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); + expect(commonUtils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); + expect(commonUtils.setParamInURL('page', true)).toBe('?scope=all&page=true'); + }); + }); - const WORKHORSE = 'X-SOMETHING-WORKHORSE'; - const NGINX = 'X-SOMETHING-NGINX'; + describe('getParameterByName', () => { + beforeEach(() => { + window.history.pushState({}, null, '?scope=all&p=2'); + }); - expect(normalized[WORKHORSE].workhorse).toBe('ok'); - expect(normalized[NGINX].nginx).toBe('ok'); - }); + afterEach(() => { + window.history.replaceState({}, null, null); }); - describe('gl.utils.normalizeCRLFHeaders', () => { - beforeEach(function () { - this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + it('should return valid parameter', () => { + const value = commonUtils.getParameterByName('scope'); + expect(commonUtils.getParameterByName('p')).toEqual('2'); + expect(value).toBe('all'); + }); - spyOn(String.prototype, 'split').and.callThrough(); - spyOn(gl.utils, 'normalizeHeaders').and.callThrough(); + it('should return invalid parameter', () => { + const value = commonUtils.getParameterByName('fakeParameter'); + expect(value).toBe(null); + }); - this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders); - }); + it('should return valid paramentes if URL is provided', () => { + let value = commonUtils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); + expect(value).toBe('bar'); - it('should split by newline', function () { - expect(String.prototype.split).toHaveBeenCalledWith('\n'); - }); + value = commonUtils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); + expect(value).toBe('canchu'); + }); + }); - it('should split by colon+space for each header', function () { - expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); - }); + describe('normalizedHeaders', () => { + it('should upperCase all the header keys to keep them consistent', () => { + const apiHeaders = { + 'X-Something-Workhorse': { workhorse: 'ok' }, + 'x-something-nginx': { nginx: 'ok' }, + }; - it('should call gl.utils.normalizeHeaders with a parsed headers object', function () { - expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object)); - }); + const normalized = commonUtils.normalizeHeaders(apiHeaders); - it('should return a normalized headers object', function () { - expect(this.normalizeCRLFHeaders).toEqual({ - 'A-HEADER': 'a-value', - 'ANOTHER-HEADER': 'ANOTHER-VALUE', - 'LAST-HEADER': 'last-VALUE', - }); - }); + const WORKHORSE = 'X-SOMETHING-WORKHORSE'; + const NGINX = 'X-SOMETHING-NGINX'; + + expect(normalized[WORKHORSE].workhorse).toBe('ok'); + expect(normalized[NGINX].nginx).toBe('ok'); }); + }); - describe('gl.utils.parseIntPagination', () => { - it('should parse to integers all string values and return pagination object', () => { - const pagination = { - 'X-PER-PAGE': 10, - 'X-PAGE': 2, - 'X-TOTAL': 30, - 'X-TOTAL-PAGES': 3, - 'X-NEXT-PAGE': 3, - 'X-PREV-PAGE': 1, - }; - - const expectedPagination = { - perPage: 10, - page: 2, - total: 30, - totalPages: 3, - nextPage: 3, - previousPage: 1, - }; - - expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination); - }); + describe('normalizeCRLFHeaders', () => { + beforeEach(function () { + this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + spyOn(String.prototype, 'split').and.callThrough(); + this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders); + }); + + it('should split by newline', function () { + expect(String.prototype.split).toHaveBeenCalledWith('\n'); }); - describe('gl.utils.isMetaClick', () => { - it('should identify meta click on Windows/Linux', () => { - const e = { - metaKey: false, - ctrlKey: true, - which: 1, - }; + it('should split by colon+space for each header', function () { + expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); + }); - expect(gl.utils.isMetaClick(e)).toBe(true); + it('should return a normalized headers object', function () { + expect(this.normalizeCRLFHeaders).toEqual({ + 'A-HEADER': 'a-value', + 'ANOTHER-HEADER': 'ANOTHER-VALUE', + 'LAST-HEADER': 'last-VALUE', }); + }); + }); + + describe('parseIntPagination', () => { + it('should parse to integers all string values and return pagination object', () => { + const pagination = { + 'X-PER-PAGE': 10, + 'X-PAGE': 2, + 'X-TOTAL': 30, + 'X-TOTAL-PAGES': 3, + 'X-NEXT-PAGE': 3, + 'X-PREV-PAGE': 1, + }; + + const expectedPagination = { + perPage: 10, + page: 2, + total: 30, + totalPages: 3, + nextPage: 3, + previousPage: 1, + }; + + expect(commonUtils.parseIntPagination(pagination)).toEqual(expectedPagination); + }); + }); - it('should identify meta click on macOS', () => { - const e = { - metaKey: true, - ctrlKey: false, - which: 1, - }; + describe('isMetaClick', () => { + it('should identify meta click on Windows/Linux', () => { + const e = { + metaKey: false, + ctrlKey: true, + which: 1, + }; - expect(gl.utils.isMetaClick(e)).toBe(true); - }); + expect(commonUtils.isMetaClick(e)).toBe(true); + }); - it('should identify as meta click on middle-click or Mouse-wheel click', () => { - const e = { - metaKey: false, - ctrlKey: false, - which: 2, - }; + it('should identify meta click on macOS', () => { + const e = { + metaKey: true, + ctrlKey: false, + which: 1, + }; - expect(gl.utils.isMetaClick(e)).toBe(true); + expect(commonUtils.isMetaClick(e)).toBe(true); + }); + + it('should identify as meta click on middle-click or Mouse-wheel click', () => { + const e = { + metaKey: false, + ctrlKey: false, + which: 2, + }; + + expect(commonUtils.isMetaClick(e)).toBe(true); + }); + }); + + describe('convertPermissionToBoolean', () => { + it('should convert a boolean in a string to a boolean', () => { + expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true); + expect(commonUtils.convertPermissionToBoolean('false')).toEqual(false); + }); + }); + + describe('backOff', () => { + beforeEach(() => { + // shortcut our timeouts otherwise these tests will take a long time to finish + const origSetTimeout = window.setTimeout; + spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); + }); + + it('solves the promise from the callback', (done) => { + const expectedResponseValue = 'Success!'; + commonUtils.backOff((next, stop) => ( + new Promise((resolve) => { + resolve(expectedResponseValue); + }).then((resp) => { + stop(resp); + }) + )).then((respBackoff) => { + expect(respBackoff).toBe(expectedResponseValue); + done(); }); }); - describe('gl.utils.backOff', () => { - beforeEach(() => { - // shortcut our timeouts otherwise these tests will take a long time to finish - const origSetTimeout = window.setTimeout; - spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); + it('catches the rejected promise from the callback ', (done) => { + const errorMessage = 'Mistakes were made!'; + commonUtils.backOff((next, stop) => { + new Promise((resolve, reject) => { + reject(new Error(errorMessage)); + }).then((resp) => { + stop(resp); + }).catch(err => stop(err)); + }).catch((errBackoffResp) => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe(errorMessage); + done(); }); + }); - it('solves the promise from the callback', (done) => { - const expectedResponseValue = 'Success!'; - gl.utils.backOff((next, stop) => ( - new Promise((resolve) => { - resolve(expectedResponseValue); - }).then((resp) => { - stop(resp); + it('solves the promise correctly after retrying a third time', (done) => { + let numberOfCalls = 1; + const expectedResponseValue = 'Success!'; + commonUtils.backOff((next, stop) => ( + Promise.resolve(expectedResponseValue) + .then((resp) => { + if (numberOfCalls < 3) { + numberOfCalls += 1; + next(); + } else { + stop(resp); + } }) - )).then((respBackoff) => { - expect(respBackoff).toBe(expectedResponseValue); - done(); - }); + )).then((respBackoff) => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + expect(timeouts).toEqual([2000, 4000]); + expect(respBackoff).toBe(expectedResponseValue); + done(); }); + }); - it('catches the rejected promise from the callback ', (done) => { - const errorMessage = 'Mistakes were made!'; - gl.utils.backOff((next, stop) => { - new Promise((resolve, reject) => { - reject(new Error(errorMessage)); - }).then((resp) => { - stop(resp); - }).catch(err => stop(err)); - }).catch((errBackoffResp) => { + it('rejects the backOff promise after timing out', (done) => { + commonUtils.backOff(next => next(), 64000) + .catch((errBackoffResp) => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe(errorMessage); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); done(); }); - }); + }); + }); - it('solves the promise correctly after retrying a third time', (done) => { - let numberOfCalls = 1; - const expectedResponseValue = 'Success!'; - gl.utils.backOff((next, stop) => ( - Promise.resolve(expectedResponseValue) - .then((resp) => { - if (numberOfCalls < 3) { - numberOfCalls += 1; - next(); - } else { - stop(resp); - } - }) - )).then((respBackoff) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000]); - expect(respBackoff).toBe(expectedResponseValue); - done(); - }); - }); + describe('setFavicon', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'default/favicon'); + document.body.appendChild(favicon); + }); - it('rejects the backOff promise after timing out', (done) => { - gl.utils.backOff(next => next(), 64000) - .catch((errBackoffResp) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); - done(); - }); - }); + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); }); + it('should set page favicon to provided favicon', () => { + const faviconPath = '//custom_favicon'; + commonUtils.setFavicon(faviconPath); - describe('gl.utils.setFavicon', () => { - it('should set page favicon to provided favicon', () => { - const faviconPath = '//custom_favicon'; - const fakeLink = { - setAttribute() {}, - }; + expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconPath); + }); + }); - spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); - spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { - expect(attr).toEqual('href'); - expect(val.indexOf(faviconPath) > -1).toBe(true); - }); - gl.utils.setFavicon(faviconPath); - }); + describe('resetFavicon', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'default/favicon'); + document.body.appendChild(favicon); }); - describe('gl.utils.resetFavicon', () => { - it('should reset page favicon to tanuki', () => { - const fakeLink = { - setAttribute() {}, - }; + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); - spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); - spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { - expect(attr).toEqual('href'); - expect(val).toMatch(/favicon/); - }); - gl.utils.resetFavicon(); + it('should reset page favicon to tanuki', () => { + commonUtils.resetFavicon(); + expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon'); + }); + }); + + describe('setCiStatusFavicon', () => { + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; + + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + document.body.appendChild(favicon); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + + it('should reset favicon in case of error', () => { + const favicon = document.getElementById('favicon'); + spyOn($, 'ajax').and.callFake(function (options) { + options.error(); + expect(favicon.getAttribute('href')).toEqual('null'); }); + + commonUtils.setCiStatusFavicon(BUILD_URL); }); - describe('gl.utils.setCiStatusFavicon', () => { - it('should set page favicon to CI status favicon based on provided status', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; - const FAVICON_PATH = '//icon_status_success'; - const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); - const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ favicon: FAVICON_PATH }); - expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH); - options.success(); - expect(spyResetFavicon).toHaveBeenCalled(); - options.error(); - expect(spyResetFavicon).toHaveBeenCalled(); - }); + it('should set page favicon to CI status favicon based on provided status', () => { + const FAVICON_PATH = '//icon_status_success'; + const favicon = document.getElementById('favicon'); - gl.utils.setCiStatusFavicon(BUILD_URL); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ favicon: FAVICON_PATH }); + expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); }); + + commonUtils.setCiStatusFavicon(BUILD_URL); }); + }); - describe('gl.utils.ajaxPost', () => { - it('should perform `$.ajax` call and do `POST` request', () => { - const requestURL = '/some/random/api'; - const data = { keyname: 'value' }; - const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); + describe('ajaxPost', () => { + it('should perform `$.ajax` call and do `POST` request', () => { + const requestURL = '/some/random/api'; + const data = { keyname: 'value' }; + const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); + + commonUtils.ajaxPost(requestURL, data); + expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); + }); + + describe('gl.utils.spriteIcon', () => { + beforeEach(() => { + window.gon.sprite_icons = 'icons.svg'; + }); - gl.utils.ajaxPost(requestURL, data); - expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); + it('should return the svg for a linked icon', () => { + expect(gl.utils.spriteIcon('test')).toEqual('<svg><use xlink:href="icons.svg#test" /></svg>'); }); }); }); -})(); +}); diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js new file mode 100644 index 00000000000..c484213df8e --- /dev/null +++ b/spec/javascripts/lib/utils/csrf_token_spec.js @@ -0,0 +1,49 @@ +import csrf from '~/lib/utils/csrf'; + +describe('csrf', () => { + beforeEach(() => { + this.tokenKey = 'X-CSRF-Token'; + this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ=='; + }); + + it('returns the correct headerKey', () => { + expect(csrf.headerKey).toBe(this.tokenKey); + }); + + describe('when csrf token is in the DOM', () => { + beforeEach(() => { + setFixtures(` + <meta name="csrf-token" content="${this.token}"> + `); + + csrf.init(); + }); + + it('returns the csrf token', () => { + expect(csrf.token).toBe(this.token); + }); + + it('returns the csrf headers object', () => { + expect(csrf.headers[this.tokenKey]).toBe(this.token); + }); + }); + + describe('when csrf token is not in the DOM', () => { + beforeEach(() => { + setFixtures(` + <meta name="some-other-token"> + `); + + csrf.init(); + }); + + it('returns null for token', () => { + expect(csrf.token).toBeNull(); + }); + + it('returns empty object for headers', () => { + expect(typeof csrf.headers).toBe('object'); + expect(Object.keys(csrf.headers).length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/sticky_spec.js b/spec/javascripts/lib/utils/sticky_spec.js index c3ee3ef9825..b87c836654d 100644 --- a/spec/javascripts/lib/utils/sticky_spec.js +++ b/spec/javascripts/lib/utils/sticky_spec.js @@ -1,52 +1,79 @@ import { isSticky } from '~/lib/utils/sticky'; describe('sticky', () => { - const el = { - offsetTop: 0, - classList: {}, - }; + let el; beforeEach(() => { - el.offsetTop = 0; - el.classList.add = jasmine.createSpy('spy'); - el.classList.remove = jasmine.createSpy('spy'); + document.body.innerHTML += ` + <div class="parent"> + <div id="js-sticky"></div> + </div> + `; + + el = document.getElementById('js-sticky'); }); - describe('classList.remove', () => { - it('does not call classList.remove when stuck', () => { - isSticky(el, 0, 0); + afterEach(() => { + el.parentNode.remove(); + }); + + describe('when stuck', () => { + it('does not remove is-stuck class', () => { + isSticky(el, 0, el.offsetTop); + isSticky(el, 0, el.offsetTop); expect( - el.classList.remove, - ).not.toHaveBeenCalled(); + el.classList.contains('is-stuck'), + ).toBeTruthy(); }); - it('calls classList.remove when not stuck', () => { - el.offsetTop = 10; - isSticky(el, 0, 0); + it('adds is-stuck class', () => { + isSticky(el, 0, el.offsetTop); expect( - el.classList.remove, - ).toHaveBeenCalledWith('is-stuck'); + el.classList.contains('is-stuck'), + ).toBeTruthy(); + }); + + it('inserts placeholder element', () => { + isSticky(el, 0, el.offsetTop, true); + + expect( + document.querySelector('.sticky-placeholder'), + ).not.toBeNull(); }); }); - describe('classList.add', () => { - it('calls classList.add when stuck', () => { + describe('when not stuck', () => { + it('removes is-stuck class', () => { + spyOn(el.classList, 'remove').and.callThrough(); + + isSticky(el, 0, el.offsetTop); isSticky(el, 0, 0); expect( - el.classList.add, + el.classList.remove, ).toHaveBeenCalledWith('is-stuck'); + expect( + el.classList.contains('is-stuck'), + ).toBeFalsy(); }); - it('does not call classList.add when not stuck', () => { - el.offsetTop = 10; + it('does not add is-stuck class', () => { isSticky(el, 0, 0); expect( - el.classList.add, - ).not.toHaveBeenCalled(); + el.classList.contains('is-stuck'), + ).toBeFalsy(); + }); + + it('removes placeholder', () => { + isSticky(el, 0, el.offsetTop, true); + isSticky(el, 0, 0, true); + + expect( + document.querySelector('.sticky-placeholder'), + ).toBeNull(); }); }); }); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index aee274641e8..645664a5219 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -18,19 +18,25 @@ import '~/line_highlighter'; beforeEach(function() { loadFixtures('static/line_highlighter.html.raw'); this["class"] = new LineHighlighter(); - this.css = this["class"].highlightClass; + this.css = this["class"].highlightLineClass; return this.spies = { __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {}) }; }); describe('behavior', function() { it('highlights one line given in the URL hash', function() { - new LineHighlighter('#L13'); + new LineHighlighter({ hash: '#L13' }); return expect($('#LC13')).toHaveClass(this.css); }); + it('highlights one line given in the URL hash with given CSS class name', function() { + const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' }); + expect(hiliter.highlightLineClass).toBe('hilite'); + expect($('#LC13')).toHaveClass('hilite'); + expect($('#LC13')).not.toHaveClass('hll'); + }); it('highlights a range of lines given in the URL hash', function() { var line, results; - new LineHighlighter('#L5-25'); + new LineHighlighter({ hash: '#L5-25' }); expect($("." + this.css).length).toBe(21); results = []; for (line = 5; line <= 25; line += 1) { @@ -41,7 +47,7 @@ import '~/line_highlighter'; it('scrolls to the first highlighted line on initial load', function() { var spy; spy = spyOn($, 'scrollTo'); - new LineHighlighter('#L5-25'); + new LineHighlighter({ hash: '#L5-25' }); return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()); }); it('discards click events', function() { @@ -50,10 +56,10 @@ import '~/line_highlighter'; clickLine(13); return expect(spy).toHaveBeenPrevented(); }); - return it('handles garbage input from the hash', function() { + it('handles garbage input from the hash', function() { var func; func = function() { - return new LineHighlighter('#blob-content-holder'); + return new LineHighlighter({ fileHolderSelector: '#blob-content-holder' }); }; return expect(func).not.toThrow(); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 8830a2d29e5..ccdbfcba692 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -78,8 +78,9 @@ import 'vendor/jquery.scrollTo'; }); describe('meta click', () => { + let metakeyEvent; beforeEach(function () { - spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); }); it('opens page when commits link is clicked', function () { @@ -89,7 +90,7 @@ import 'vendor/jquery.scrollTo'; }); this.class.bindEvents(); - document.querySelector('.merge-request-tabs .commits-tab a').click(); + $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); }); it('opens page when commits badge is clicked', function () { @@ -99,7 +100,7 @@ import 'vendor/jquery.scrollTo'; }); this.class.bindEvents(); - document.querySelector('.merge-request-tabs .commits-tab a .badge').click(); + $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); }); }); @@ -415,5 +416,28 @@ import 'vendor/jquery.scrollTo'; }); }); }); + + describe('expandViewContainer', function () { + beforeEach(() => { + $('body').append('<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>'); + }); + + afterEach(() => { + $('.content-wrapper').remove(); + }); + + it('removes container-limited from containers', function () { + this.class.expandViewContainer(); + + expect($('.content-wrapper')).not.toContainElement('.container-limited'); + }); + + it('does remove container-limited from breadcrumbs', function () { + $('.container-limited').addClass('breadcrumbs'); + this.class.expandViewContainer(); + + expect($('.content-wrapper')).toContainElement('.container-limited'); + }); + }); }); }).call(window); diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js index e8f7042e131..3319eeb3f31 100644 --- a/spec/javascripts/monitoring/dashboard_state_spec.js +++ b/spec/javascripts/monitoring/dashboard_state_spec.js @@ -21,6 +21,9 @@ describe('EmptyState', () => { selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.currentState).toBe(component.states.gettingStarted); @@ -31,6 +34,9 @@ describe('EmptyState', () => { selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.buttonPath).toEqual(statePaths.settingsPath); @@ -42,6 +48,9 @@ describe('EmptyState', () => { selectedState: 'loading', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.buttonPath).toEqual(statePaths.documentationPath); @@ -53,6 +62,9 @@ describe('EmptyState', () => { selectedState: 'unableToConnect', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.showButtonDescription).toEqual(true); @@ -63,6 +75,9 @@ describe('EmptyState', () => { selectedState: 'loading', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.showButtonDescription).toEqual(false); @@ -74,6 +89,9 @@ describe('EmptyState', () => { selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.$el.querySelector('svg')).toBeDefined(); @@ -87,6 +105,9 @@ describe('EmptyState', () => { selectedState: 'loading', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.$el.querySelector('svg')).toBeDefined(); @@ -100,6 +121,9 @@ describe('EmptyState', () => { selectedState: 'unableToConnect', settingsPath: statePaths.settingsPath, documentationPath: statePaths.documentationPath, + emptyGettingStartedSvgPath: 'foo', + emptyLoadingSvgPath: 'foo', + emptyUnableToConnectSvgPath: 'foo', }); expect(component.$el.querySelector('svg')).toBeDefined(); diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index cca5ec887a3..1c8b1b98242 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import autosize from 'vendor/autosize'; import store from '~/notes/stores'; import issueCommentForm from '~/notes/components/issue_comment_form.vue'; import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data'; @@ -55,6 +56,19 @@ describe('issue_comment_form component', () => { expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions'); }); + it('should resize textarea after note discarded', (done) => { + spyOn(autosize, 'update'); + spyOn(vm, 'discard').and.callThrough(); + + vm.note = 'foo'; + vm.discard(); + + Vue.nextTick(() => { + expect(autosize.update).toHaveBeenCalled(); + done(); + }); + }); + describe('edit mode', () => { it('should enter edit mode when arrow up is pressed', () => { spyOn(vm, 'editCurrentUserLastNote').and.callThrough(); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 72d362acb2f..2b2219dcf0c 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,4 +1,3 @@ - import * as actions from '~/notes/stores/actions'; import testAction from './helpers'; import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index a38f29c1e39..1e22e03e178 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -3,19 +3,31 @@ import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, indiv describe('Mutation Notes Store', () => { describe('ADD_NEW_NOTE', () => { - it('should add a new note to an array of notes', () => { - const state = { notes: [] }; + let state; + let noteData; + + beforeEach(() => { + state = { notes: [] }; + noteData = { + expanded: true, + id: note.discussion_id, + individual_note: true, + notes: [note], + reply_id: note.discussion_id, + }; mutations.ADD_NEW_NOTE(state, note); + }); + it('should add a new note to an array of notes', () => { expect(state).toEqual({ - notes: [{ - expanded: true, - id: note.discussion_id, - individual_note: true, - notes: [note], - reply_id: note.discussion_id, - }], + notes: [noteData], }); + expect(state.notes.length).toBe(1); + }); + + it('should not add the same note to the notes array', () => { + mutations.ADD_NEW_NOTE(state, note); + expect(state.notes.length).toBe(1); }); }); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index bb47a28d9fe..6611b74594f 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -11,6 +11,7 @@ describe('Pipelines Empty State', () => { component = new EmptyStateComponent({ propsData: { helpPagePath: 'foo', + emptyStateSvgPath: 'foo', }, }).$mount(); }); diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js index f667d351f72..a402857a4d1 100644 --- a/spec/javascripts/pipelines/error_state_spec.js +++ b/spec/javascripts/pipelines/error_state_spec.js @@ -8,7 +8,11 @@ describe('Pipelines Error State', () => { beforeEach(() => { ErrorStateComponent = Vue.extend(errorStateComp); - component = new ErrorStateComponent().$mount(); + component = new ErrorStateComponent({ + propsData: { + errorStateSvgPath: 'foo', + }, + }).$mount(); }); it('should render error state SVG', () => { diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js index acb67d0ec21..a8a8e3e2cff 100644 --- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js +++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js @@ -34,7 +34,7 @@ describe('Pipelines Artifacts dropdown', () => { ).toEqual(artifacts[0].path); expect( - component.$el.querySelector('.dropdown-menu li a span').textContent, + component.$el.querySelector('.dropdown-menu li a').textContent, ).toContain(artifacts[0].name); }); }); diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js index 0a6c479a95b..084ffe08917 100644 --- a/spec/javascripts/pretty_time_spec.js +++ b/spec/javascripts/pretty_time_spec.js @@ -1,215 +1,133 @@ -import '~/lib/utils/pretty_time'; +import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time'; -(() => { - const prettyTime = gl.utils.prettyTime; +function assertTimeUnits(obj, minutes, hours, days, weeks) { + expect(obj.minutes).toBe(minutes); + expect(obj.hours).toBe(hours); + expect(obj.days).toBe(days); + expect(obj.weeks).toBe(weeks); +} - describe('prettyTime methods', function () { - describe('parseSeconds', function () { - it('should correctly parse a negative value', function () { - const parser = prettyTime.parseSeconds; +describe('prettyTime methods', () => { + describe('parseSeconds', () => { + it('should correctly parse a negative value', () => { + const zeroSeconds = parseSeconds(-1000); - const zeroSeconds = parser(-1000); - - expect(zeroSeconds.minutes).toBe(16); - expect(zeroSeconds.hours).toBe(0); - expect(zeroSeconds.days).toBe(0); - expect(zeroSeconds.weeks).toBe(0); - }); - - it('should correctly parse a zero value', function () { - const parser = prettyTime.parseSeconds; - - const zeroSeconds = parser(0); - - expect(zeroSeconds.minutes).toBe(0); - expect(zeroSeconds.hours).toBe(0); - expect(zeroSeconds.days).toBe(0); - expect(zeroSeconds.weeks).toBe(0); - }); - - it('should correctly parse a small non-zero second values', function () { - const parser = prettyTime.parseSeconds; - - const subOneMinute = parser(10); - - expect(subOneMinute.minutes).toBe(0); - expect(subOneMinute.hours).toBe(0); - expect(subOneMinute.days).toBe(0); - expect(subOneMinute.weeks).toBe(0); - - const aboveOneMinute = parser(100); - - expect(aboveOneMinute.minutes).toBe(1); - expect(aboveOneMinute.hours).toBe(0); - expect(aboveOneMinute.days).toBe(0); - expect(aboveOneMinute.weeks).toBe(0); - - const manyMinutes = parser(1000); - - expect(manyMinutes.minutes).toBe(16); - expect(manyMinutes.hours).toBe(0); - expect(manyMinutes.days).toBe(0); - expect(manyMinutes.weeks).toBe(0); - }); - - it('should correctly parse large second values', function () { - const parser = prettyTime.parseSeconds; - - const aboveOneHour = parser(4800); - - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); - - const aboveOneDay = parser(110000); - - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(6); - expect(aboveOneDay.days).toBe(3); - expect(aboveOneDay.weeks).toBe(0); - - const aboveOneWeek = parser(25000000); - - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(0); - expect(aboveOneWeek.days).toBe(3); - expect(aboveOneWeek.weeks).toBe(173); - }); + assertTimeUnits(zeroSeconds, 16, 0, 0, 0); + }); - it('should correctly accept a custom param for hoursPerDay', function () { - const parser = prettyTime.parseSeconds; - const config = { hoursPerDay: 24 }; + it('should correctly parse a zero value', () => { + const zeroSeconds = parseSeconds(0); - const aboveOneHour = parser(4800, config); + assertTimeUnits(zeroSeconds, 0, 0, 0, 0); + }); - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); + it('should correctly parse a small non-zero second values', () => { + const subOneMinute = parseSeconds(10); + const aboveOneMinute = parseSeconds(100); + const manyMinutes = parseSeconds(1000); - const aboveOneDay = parser(110000, config); + assertTimeUnits(subOneMinute, 0, 0, 0, 0); + assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); + assertTimeUnits(manyMinutes, 16, 0, 0, 0); + }); - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(6); - expect(aboveOneDay.days).toBe(1); - expect(aboveOneDay.weeks).toBe(0); + it('should correctly parse large second values', () => { + const aboveOneHour = parseSeconds(4800); + const aboveOneDay = parseSeconds(110000); + const aboveOneWeek = parseSeconds(25000000); - const aboveOneWeek = parser(25000000, config); + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); + }); - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(8); - expect(aboveOneWeek.days).toBe(4); + it('should correctly accept a custom param for hoursPerDay', () => { + const config = { hoursPerDay: 24 }; - expect(aboveOneWeek.weeks).toBe(57); - }); + const aboveOneHour = parseSeconds(4800, config); + const aboveOneDay = parseSeconds(110000, config); + const aboveOneWeek = parseSeconds(25000000, config); - it('should correctly accept a custom param for daysPerWeek', function () { - const parser = prettyTime.parseSeconds; - const config = { daysPerWeek: 7 }; + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 1, 0); + assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); + }); - const aboveOneHour = parser(4800, config); + it('should correctly accept a custom param for daysPerWeek', () => { + const config = { daysPerWeek: 7 }; - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); + const aboveOneHour = parseSeconds(4800, config); + const aboveOneDay = parseSeconds(110000, config); + const aboveOneWeek = parseSeconds(25000000, config); - const aboveOneDay = parser(110000, config); + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); + }); - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(6); - expect(aboveOneDay.days).toBe(3); - expect(aboveOneDay.weeks).toBe(0); + it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { + const config = { daysPerWeek: 55, hoursPerDay: 14 }; - const aboveOneWeek = parser(25000000, config); + const aboveOneHour = parseSeconds(4800, config); + const aboveOneDay = parseSeconds(110000, config); + const aboveOneWeek = parseSeconds(25000000, config); - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(0); - expect(aboveOneWeek.days).toBe(0); + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 2, 2, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); + }); + }); - expect(aboveOneWeek.weeks).toBe(124); - }); + describe('stringifyTime', () => { + it('should stringify values with all non-zero units', () => { + const timeObject = { + weeks: 1, + days: 4, + hours: 7, + minutes: 20, + }; - it('should correctly accept custom params for daysPerWeek and hoursPerDay', function () { - const parser = prettyTime.parseSeconds; - const config = { daysPerWeek: 55, hoursPerDay: 14 }; + const timeString = stringifyTime(timeObject); - const aboveOneHour = parser(4800, config); + expect(timeString).toBe('1w 4d 7h 20m'); + }); - expect(aboveOneHour.minutes).toBe(20); - expect(aboveOneHour.hours).toBe(1); - expect(aboveOneHour.days).toBe(0); - expect(aboveOneHour.weeks).toBe(0); + it('should stringify values with some non-zero units', () => { + const timeObject = { + weeks: 0, + days: 4, + hours: 0, + minutes: 20, + }; - const aboveOneDay = parser(110000, config); + const timeString = stringifyTime(timeObject); - expect(aboveOneDay.minutes).toBe(33); - expect(aboveOneDay.hours).toBe(2); - expect(aboveOneDay.days).toBe(2); - expect(aboveOneDay.weeks).toBe(0); + expect(timeString).toBe('4d 20m'); + }); - const aboveOneWeek = parser(25000000, config); + it('should stringify values with no non-zero units', () => { + const timeObject = { + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + }; - expect(aboveOneWeek.minutes).toBe(26); - expect(aboveOneWeek.hours).toBe(0); - expect(aboveOneWeek.days).toBe(1); + const timeString = stringifyTime(timeObject); - expect(aboveOneWeek.weeks).toBe(9); - }); + expect(timeString).toBe('0m'); }); + }); - describe('stringifyTime', function () { - it('should stringify values with all non-zero units', function () { - const timeObject = { - weeks: 1, - days: 4, - hours: 7, - minutes: 20, - }; - - const timeString = prettyTime.stringifyTime(timeObject); - - expect(timeString).toBe('1w 4d 7h 20m'); - }); - - it('should stringify values with some non-zero units', function () { - const timeObject = { - weeks: 0, - days: 4, - hours: 0, - minutes: 20, - }; - - const timeString = prettyTime.stringifyTime(timeObject); - - expect(timeString).toBe('4d 20m'); - }); - - it('should stringify values with no non-zero units', function () { - const timeObject = { - weeks: 0, - days: 0, - hours: 0, - minutes: 0, - }; - - const timeString = prettyTime.stringifyTime(timeObject); - - expect(timeString).toBe('0m'); - }); + describe('abbreviateTime', () => { + it('should abbreviate stringified times for weeks', () => { + const fullTimeString = '1w 3d 4h 5m'; + expect(abbreviateTime(fullTimeString)).toBe('1w'); }); - describe('abbreviateTime', function () { - it('should abbreviate stringified times for weeks', function () { - const fullTimeString = '1w 3d 4h 5m'; - expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w'); - }); - - it('should abbreviate stringified times for non-weeks', function () { - const fullTimeString = '0w 3d 4h 5m'; - expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d'); - }); + it('should abbreviate stringified times for non-weeks', () => { + const fullTimeString = '0w 3d 4h 5m'; + expect(abbreviateTime(fullTimeString)).toBe('3d'); }); }); -})(window.gl || (window.gl = {})); +}); diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js index 29dc2d21e4b..411514009dc 100644 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ b/spec/javascripts/repo/components/repo_edit_button_spec.js @@ -21,13 +21,11 @@ describe('RepoEditButton', () => { expect(vm.$el.textContent).toMatch('Edit'); spyOn(vm, 'editCancelClicked').and.callThrough(); - spyOn(vm, 'toggleProjectRefsForm'); vm.$el.click(); Vue.nextTick(() => { expect(vm.editCancelClicked).toHaveBeenCalled(); - expect(vm.toggleProjectRefsForm).toHaveBeenCalled(); expect(vm.$el.textContent).toMatch('Cancel edit'); done(); }); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js index 518a2d25ecf..f15633bd8b9 100644 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ b/spec/javascripts/repo/components/repo_file_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import repoFile from '~/repo/components/repo_file.vue'; +import RepoStore from '~/repo/stores/repo_store'; describe('RepoFile', () => { const updated = 'updated'; @@ -12,8 +13,13 @@ describe('RepoFile', () => { level: 10, }; const activeFile = { + pageTitle: 'pageTitle', url: 'url', }; + const otherFile = { + html: '<p class="file-content">html</p>', + pageTitle: 'otherpageTitle', + }; function createComponent(propsData) { const RepoFile = Vue.extend(repoFile); @@ -60,6 +66,12 @@ describe('RepoFile', () => { expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); }); + it('sets the document title correctly', () => { + RepoStore.setActiveFiles(otherFile); + + expect(document.title.trim()).toEqual(otherFile.pageTitle); + }); + it('renders a spinner if the file is loading', () => { file.loading = true; const vm = createComponent({ diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index abcff8e537e..db9911c7a2c 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -79,6 +79,20 @@ describe('RepoSidebar', () => { expect(Helper.getContent).toHaveBeenCalledWith(file1); }); + it('should not fetch data for already opened files', () => { + const file = { + id: 42, + url: 'foo', + }; + + spyOn(Helper, 'getFileFromPath').and.returnValue(file); + spyOn(RepoStore, 'setActiveFiles'); + const vm = createComponent(); + vm.fileClicked(file); + + expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file); + }); + it('should hide files in directory if already open', () => { spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); const file1 = { diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index fd492159081..7d3c9319a11 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -26,37 +26,30 @@ describe('Todos', () => { describe('meta click', () => { let visitUrlSpy; + let windowOpenSpy; + let metakeyEvent; beforeEach(() => { - spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + windowOpenSpy = spyOn(window, 'open').and.callFake(() => {}); }); - it('opens the todo url in another tab', (done) => { + it('opens the todo url in another tab', () => { const todoLink = todoItem.dataset.url; - spyOn(window, 'open').and.callFake((url, target) => { - expect(todoLink).toEqual(url); - expect(target).toEqual('_blank'); - done(); - }); + $('.todos-list .todo').trigger(metakeyEvent); - todoItem.click(); expect(visitUrlSpy).not.toHaveBeenCalled(); + expect(windowOpenSpy).toHaveBeenCalledWith(todoLink, '_blank'); }); - it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => { - const avatarImage = todoItem.querySelector('img'); - const avatarUrl = avatarImage.parentElement.getAttribute('href'); + it('run native funcionality when avatar is clicked', () => { + $('.todos-list a').on('click', e => e.preventDefault()); + $('.todos-list img').trigger(metakeyEvent); - spyOn(window, 'open').and.callFake((url, target) => { - expect(avatarUrl).toEqual(url); - expect(target).toEqual('_blank'); - done(); - }); - - avatarImage.click(); expect(visitUrlSpy).not.toHaveBeenCalled(); + expect(windowOpenSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js deleted file mode 100644 index 69cb93bd850..00000000000 --- a/spec/javascripts/user_callout_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import Cookies from 'js-cookie'; -import UserCallout from '~/user_callout'; - -const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; - -describe('UserCallout', function () { - const fixtureName = 'dashboard/user-callout.html.raw'; - preloadFixtures(fixtureName); - - beforeEach(() => { - loadFixtures(fixtureName); - Cookies.remove(USER_CALLOUT_COOKIE); - - this.userCallout = new UserCallout(); - this.closeButton = $('.js-close-callout.close'); - this.userCalloutBtn = $('.js-close-callout:not(.close)'); - }); - - it('hides when user clicks on the dismiss-icon', (done) => { - this.closeButton.click(); - expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); - - setTimeout(() => { - expect( - document.querySelector('.user-callout'), - ).toBeNull(); - - done(); - }); - }); - - it('hides when user clicks on the "check it out" button', () => { - this.userCalloutBtn.click(); - expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); - }); - - describe('Sets cookie with setCalloutPerProject', () => { - beforeEach(() => { - spyOn(Cookies, 'set').and.callFake(() => {}); - document.querySelector('.user-callout').setAttribute('data-project-path', 'foo/bar'); - this.userCallout = new UserCallout({ setCalloutPerProject: true }); - }); - - it('sets a cookie when the user clicks the close button', () => { - this.userCalloutBtn.click(); - expect(Cookies.set).toHaveBeenCalledWith('user_callout_dismissed', 'true', Object({ expires: 365, path: 'foo/bar' })); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index da66c7504cb..e4324e91502 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -224,29 +224,41 @@ describe('mrWidgetOptions', () => { describe('handleMounted', () => { it('should call required methods to do the initial kick-off', () => { spyOn(vm, 'initDeploymentsPolling'); - spyOn(vm, 'setFavicon'); + spyOn(vm, 'setFaviconHelper'); vm.handleMounted(); - expect(vm.setFavicon).toHaveBeenCalled(); + expect(vm.setFaviconHelper).toHaveBeenCalled(); expect(vm.initDeploymentsPolling).toHaveBeenCalled(); }); }); describe('setFavicon', () => { + let faviconElement; + + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + document.body.appendChild(favicon); + + faviconElement = document.getElementById('favicon'); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + it('should call setFavicon method', () => { - spyOn(gl.utils, 'setFavicon'); - vm.setFavicon(); + vm.setFaviconHelper(); - expect(gl.utils.setFavicon).toHaveBeenCalledWith(vm.mr.ciStatusFaviconPath); + expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath); }); it('should not call setFavicon when there is no ciStatusFaviconPath', () => { - spyOn(gl.utils, 'setFavicon'); vm.mr.ciStatusFaviconPath = null; - vm.setFavicon(); + vm.setFaviconHelper(); - expect(gl.utils.setFavicon).not.toHaveBeenCalled(); + expect(faviconElement.getAttribute('href')).toEqual(null); }); }); diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index ebd6c79077e..fe7a8c84c9e 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -296,7 +296,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do context 'project milestones' do let(:milestone) { create(:milestone, project: project) } - let(:reference) { milestone.to_reference } + let(:reference) { milestone.to_reference(format: :iid) } include_examples 'reference parsing' diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 01ceb21dfaa..5f41e28fece 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -47,9 +47,11 @@ describe Banzai::Filter::SanitizationFilter do describe 'custom whitelist' do it 'customizes the whitelist only once' do instance = described_class.new('Foo') + control_count = instance.whitelist[:transformers].size + 3.times { instance.whitelist } - expect(instance.whitelist[:transformers].size).to eq 5 + expect(instance.whitelist[:transformers].size).to eq control_count end it 'sanitizes `class` attribute from all elements' do @@ -101,16 +103,18 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(act).to_html).to eq exp end - it 'disallows the `name` attribute globally' do + it 'disallows the `name` attribute globally, allows on `a`' do html = <<~HTML <img name="getElementById" src=""> <span name="foo" class="bar">Hi</span> + <a name="foo" class="bar">Bye</a> HTML doc = filter(html) expect(doc.at_css('img')).not_to have_attribute('name') expect(doc.at_css('span')).not_to have_attribute('name') + expect(doc.at_css('a')).to have_attribute('name') end it 'allows `summary` elements' do diff --git a/spec/lib/github/client_spec.rb b/spec/lib/github/client_spec.rb new file mode 100644 index 00000000000..b846096fe25 --- /dev/null +++ b/spec/lib/github/client_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Github::Client do + let(:connection) { spy } + let(:rate_limit) { double(get: [false, 1]) } + let(:client) { described_class.new({}) } + let(:results) { double } + let(:response) { double } + + before do + allow(Faraday).to receive(:new).and_return(connection) + allow(Github::RateLimit).to receive(:new).with(connection).and_return(rate_limit) + end + + describe '#get' do + before do + allow(Github::Response).to receive(:new).with(results).and_return(response) + end + + it 'uses a default per_page param' do + expect(connection).to receive(:get).with('/foo', per_page: 100).and_return(results) + + expect(client.get('/foo')).to eq(response) + end + + context 'with per_page given' do + it 'overwrites the default per_page' do + expect(connection).to receive(:get).with('/foo', per_page: 30).and_return(results) + + expect(client.get('/foo', per_page: 30)).to eq(response) + end + end + end +end diff --git a/spec/lib/github/import/legacy_diff_note_spec.rb b/spec/lib/github/import/legacy_diff_note_spec.rb new file mode 100644 index 00000000000..8c50b46cacb --- /dev/null +++ b/spec/lib/github/import/legacy_diff_note_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Github::Import::LegacyDiffNote do + describe '#type' do + it 'returns the original note type' do + expect(described_class.new.type).to eq('LegacyDiffNote') + end + end +end diff --git a/spec/lib/github/import/note_spec.rb b/spec/lib/github/import/note_spec.rb new file mode 100644 index 00000000000..fcdccd9e097 --- /dev/null +++ b/spec/lib/github/import/note_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Github::Import::Note do + describe '#type' do + it 'returns the original note type' do + expect(described_class.new.type).to eq('Note') + end + end +end diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index c0427639746..d2e7243ee05 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do +describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate do describe '#perform' do - set(:merge_request) { create(:merge_request) } - set(:merge_request_diff) { merge_request.merge_request_diff } + let(:merge_request) { create(:merge_request) } + let(:merge_request_diff) { merge_request.merge_request_diff } let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) } def diffs_to_hashes(diffs) @@ -70,8 +70,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do before do merge_request.reload_diff(true) - convert_to_yaml(start_id, merge_request_diff.commits, merge_request_diff.diffs) - convert_to_yaml(stop_id, updated_merge_request_diff.commits, updated_merge_request_diff.diffs) + convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files)) + convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files)) MergeRequestDiffCommit.delete_all MergeRequestDiffFile.delete_all @@ -80,10 +80,32 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when BUFFER_ROWS is exceeded' do before do stub_const("#{described_class}::BUFFER_ROWS", 1) + + allow(Gitlab::Database).to receive(:bulk_insert).and_call_original + end + + it 'inserts commit rows in chunks of BUFFER_ROWS' do + # There are 29 commits in each diff, so we should have slices of 20 + 9 + 20 + 9. + stub_const("#{described_class}::BUFFER_ROWS", 20) + + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_commits', anything) + .exactly(4) + .times + .and_call_original + + subject.perform(start_id, stop_id) end - it 'updates and continues' do - expect(described_class::MergeRequestDiff).to receive(:transaction).twice + it 'inserts diff rows in chunks of DIFF_FILE_BUFFER_ROWS' do + # There are 20 files in each diff, so we should have slices of 20 + 20. + stub_const("#{described_class}::DIFF_FILE_BUFFER_ROWS", 20) + + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_files', anything) + .exactly(2) + .times + .and_call_original subject.perform(start_id, stop_id) end @@ -91,27 +113,87 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do context 'when BUFFER_ROWS is not exceeded' do it 'only updates once' do - expect(described_class::MergeRequestDiff).to receive(:transaction).once + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_commits', anything) + .once + .and_call_original + + expect(Gitlab::Database).to receive(:bulk_insert) + .with('merge_request_diff_files', anything) + .once + .and_call_original subject.perform(start_id, stop_id) end end - end - context 'when the merge request diff update fails' do - before do - allow(described_class::MergeRequestDiff) - .to receive(:update_all).and_raise(ActiveRecord::Rollback) - end + context 'when some rows were already inserted due to a previous failure' do + before do + subject.perform(start_id, stop_id) - it 'does not add any diff commits' do - expect { subject.perform(merge_request_diff.id, merge_request_diff.id) } - .not_to change { MergeRequestDiffCommit.count } + convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files)) + convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files)) + end + + it 'does not raise' do + expect { subject.perform(start_id, stop_id) }.not_to raise_exception + end + + it 'logs a message' do + expect(Rails.logger).to receive(:info) + .with( + a_string_matching(described_class.name).and(matching([start_id, stop_id].inspect)) + ) + .twice + + subject.perform(start_id, stop_id) + end + + it 'ends up with the correct rows' do + expect(updated_merge_request_diff.commits.count).to eq(29) + expect(updated_merge_request_diff.raw_diffs.count).to eq(20) + end end - it 'does not add any diff files' do - expect { subject.perform(merge_request_diff.id, merge_request_diff.id) } - .not_to change { MergeRequestDiffFile.count } + context 'when the merge request diff update fails' do + let(:exception) { ActiveRecord::RecordNotFound } + + let(:perform_ignoring_exceptions) do + begin + subject.perform(start_id, stop_id) + rescue described_class::Error + end + end + + before do + allow_any_instance_of(described_class::MergeRequestDiff::ActiveRecord_Relation) + .to receive(:update_all).and_raise(exception) + end + + it 'raises an error' do + expect { subject.perform(start_id, stop_id) } + .to raise_exception(described_class::Error) + end + + it 'logs the error' do + expect(Rails.logger).to receive(:info).with( + a_string_matching(described_class.name) + .and(matching([start_id, stop_id].inspect)) + .and(matching(exception.name)) + ) + + perform_ignoring_exceptions + end + + it 'still adds diff commits' do + expect { perform_ignoring_exceptions } + .to change { MergeRequestDiffCommit.count } + end + + it 'still adds diff files' do + expect { perform_ignoring_exceptions } + .to change { MergeRequestDiffFile.count } + end end end diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index 8772d3d5ada..422f2af7266 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -26,6 +26,9 @@ describe Backup::Manager do [ '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', '1451520000_2015_12_31_4.5.6_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar', '1451510000_2015_12_30_gitlab_backup.tar', '1450742400_2015_12_22_gitlab_backup.tar', '1449878400_gitlab_backup.tar', @@ -57,6 +60,30 @@ describe Backup::Manager do end end + context 'when no valid file is found' do + let(:files) do + [ + '14516064000_2016_01_01_1.2.3_gitlab_backup.tar', + 'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar' + ] + end + + before do + allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) + + subject.remove_old + end + + it 'removes no files' do + expect(FileUtils).not_to have_received(:rm) + end + + it 'prints a done message' do + expect(progress).to have_received(:puts).with('done. (0 removed)') + end + end + context 'when there are no files older than keep_time' do before do # Set to 30 days @@ -84,16 +111,22 @@ describe Backup::Manager do it 'removes matching files with a human-readable versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[1]) - end - - it 'removes matching files with a human-readable non-versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[2]) expect(FileUtils).to have_received(:rm).with(files[3]) end - it 'removes matching files without a human-readable timestamp' do + it 'removes matching files with a human-readable versioned timestamp with tagged EE' do expect(FileUtils).to have_received(:rm).with(files[4]) + end + + it 'removes matching files with a human-readable non-versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[5]) + expect(FileUtils).to have_received(:rm).with(files[6]) + end + + it 'removes matching files without a human-readable timestamp' do + expect(FileUtils).to have_received(:rm).with(files[7]) + expect(FileUtils).to have_received(:rm).with(files[8]) end it 'does not remove files that are not old enough' do @@ -101,11 +134,11 @@ describe Backup::Manager do end it 'does not remove non-matching files' do - expect(FileUtils).not_to have_received(:rm).with(files[6]) + expect(FileUtils).not_to have_received(:rm).with(files[9]) end it 'prints a done message' do - expect(progress).to have_received(:puts).with('done. (5 removed)') + expect(progress).to have_received(:puts).with('done. (8 removed)') end end @@ -121,14 +154,15 @@ describe Backup::Manager do end it 'removes the remaining expected files' do - expect(FileUtils).to have_received(:rm).with(files[2]) - expect(FileUtils).to have_received(:rm).with(files[3]) expect(FileUtils).to have_received(:rm).with(files[4]) expect(FileUtils).to have_received(:rm).with(files[5]) + expect(FileUtils).to have_received(:rm).with(files[6]) + expect(FileUtils).to have_received(:rm).with(files[7]) + expect(FileUtils).to have_received(:rm).with(files[8]) end it 'sets the correct removed count' do - expect(progress).to have_received(:puts).with('done. (4 removed)') + expect(progress).to have_received(:puts).with('done. (7 removed)') end it 'prints the error from file that could not be removed' do diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index f8c8b83a3ac..2c7ef622c51 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -5,13 +5,13 @@ describe Gitlab::Checks::ForcePush do context "exit code checking", skip_gitaly_mock: true do it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do - allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0]) expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error end it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do - allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1]) expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) end diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb new file mode 100644 index 00000000000..15eb01eb472 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy::Kubernetes do + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when kubernetes service is active' do + set(:project) { create(:kubernetes_project) } + + it 'is satisfied by a kubernetes pipeline' do + expect(described_class.new('active')) + .to be_satisfied_by(pipeline) + end + end + + context 'when kubernetes service is inactive' do + set(:project) { create(:project) } + + it 'is not satisfied by a pipeline without kubernetes available' do + expect(described_class.new('active')) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when kubernetes policy is invalid' do + it 'raises an error' do + expect { described_class.new('unknown') } + .to raise_error(described_class::UnknownPolicyError) + end + end +end diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb new file mode 100644 index 00000000000..7211187e511 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy::Refs do + describe '#satisfied_by?' do + context 'when matching ref' do + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'master') } + + it 'is satisfied when pipeline branch matches' do + expect(described_class.new(%w[master deploy])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when pipeline branch does not match' do + expect(described_class.new(%w[feature fix])) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when maching tags' do + context 'when pipeline runs for a tag' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'feature', tag: true) + end + + it 'is satisfied when tags matcher is specified' do + expect(described_class.new(%w[master tags])) + .to be_satisfied_by(pipeline) + end + end + + context 'when pipeline is not created for a tag' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'feature', tag: false) + end + + it 'is not satisfied when tag match is specified' do + expect(described_class.new(%w[master tags])) + .not_to be_satisfied_by(pipeline) + end + end + end + + context 'when also matching a path' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'master') + end + + it 'is satisfied when provided patch matches specified one' do + expect(described_class.new(%W[master@#{pipeline.project_full_path}])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when path differs' do + expect(described_class.new(%w[master@some/fork/repository])) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when maching a source' do + let(:pipeline) { build_stubbed(:ci_pipeline, source: :push) } + + it 'is satisifed when provided source keyword matches' do + expect(described_class.new(%w[pushes])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when provided source keyword does not match' do + expect(described_class.new(%w[triggers])) + .not_to be_satisfied_by(pipeline) + end + end + + context 'when matching a ref by a regular expression' do + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'docs-something') } + + it 'is satisfied when regexp matches pipeline ref' do + expect(described_class.new(['/docs-.*/'])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied when regexp does not match pipeline ref' do + expect(described_class.new(['/fix-.*/'])) + .not_to be_satisfied_by(pipeline) + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/policy_spec.rb b/spec/lib/gitlab/ci/build/policy_spec.rb new file mode 100644 index 00000000000..20ee3dd3e89 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy do + let(:policy) { spy('policy specification') } + + before do + stub_const("#{described_class}::Something", policy) + end + + describe '.fabricate' do + context 'when policy exists' do + it 'fabricates and initializes relevant policy' do + specs = described_class.fabricate(something: 'some value') + + expect(specs).to be_an Array + expect(specs).to be_one + expect(policy).to have_received(:new).with('some value') + end + end + + context 'when some policies are not defined' do + it 'gracefully skips unknown policies' do + expect { described_class.fabricate(unknown: 'first') } + .to raise_error(NameError) + end + end + + context 'when passing a nil value as specs' do + it 'returns an empty array' do + specs = described_class.fabricate(nil) + + expect(specs).to be_an Array + expect(specs).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb new file mode 100644 index 00000000000..f54e2326b06 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Create do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:pipeline) do + build(:ci_pipeline_with_one_job, project: project, + ref: 'master') + end + + let(:command) do + double('command', project: project, + current_user: user, + seeds_block: nil) + end + + let(:step) { described_class.new(pipeline, command) } + + before do + step.perform! + end + + context 'when pipeline is ready to be saved' do + it 'saves a pipeline' do + expect(pipeline).to be_persisted + end + + it 'does not break the chain' do + expect(step.break?).to be false + end + + it 'creates stages' do + expect(pipeline.reload.stages).to be_one + end + end + + context 'when pipeline has validation errors' do + let(:pipeline) do + build(:ci_pipeline, project: project, ref: nil) + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'appends validation error' do + expect(pipeline.errors.to_a) + .to include /Failed to persist the pipeline/ + end + end + + context 'when there is a seed block present' do + let(:seeds) { spy('pipeline seeds') } + + let(:command) do + double('command', project: project, + current_user: user, + seeds_block: seeds) + end + + it 'executes the block' do + expect(seeds).to have_received(:call).with(pipeline) + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb new file mode 100644 index 00000000000..e165e0fac2a --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Sequence do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:pipeline) { build_stubbed(:ci_pipeline) } + let(:command) { double('command' ) } + let(:first_step) { spy('first step') } + let(:second_step) { spy('second step') } + let(:sequence) { [first_step, second_step] } + + subject do + described_class.new(pipeline, command, sequence) + end + + context 'when one of steps breaks the chain' do + before do + allow(first_step).to receive(:break?).and_return(true) + end + + it 'does not process the second step' do + subject.build! do |pipeline, sequence| + expect(sequence).not_to be_complete + end + + expect(second_step).not_to have_received(:perform!) + end + + it 'returns a pipeline object' do + expect(subject.build!).to eq pipeline + end + end + + context 'when all chains are executed correctly' do + before do + sequence.each do |step| + allow(step).to receive(:break?).and_return(false) + end + end + + it 'iterates through entire sequence' do + subject.build! do |pipeline, sequence| + expect(sequence).to be_complete + end + + expect(first_step).to have_received(:perform!) + expect(second_step).to have_received(:perform!) + end + + it 'returns a pipeline object' do + expect(subject.build!).to eq pipeline + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb new file mode 100644 index 00000000000..32bd5de829b --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Skip do + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:pipeline) { create(:ci_pipeline, project: project) } + + let(:command) do + double('command', project: project, + current_user: user, + ignore_skip_ci: false, + save_incompleted: true) + end + + let(:step) { described_class.new(pipeline, command) } + + context 'when pipeline has been skipped by a user' do + before do + allow(pipeline).to receive(:git_commit_message) + .and_return('commit message [ci skip]') + + step.perform! + end + + it 'should break the chain' do + expect(step.break?).to be true + end + + it 'skips the pipeline' do + expect(pipeline.reload).to be_skipped + end + end + + context 'when pipeline has not been skipped' do + before do + step.perform! + end + + it 'should not break the chain' do + expect(step.break?).to be false + end + + it 'should not skip a pipeline chain' do + expect(pipeline.reload).not_to be_skipped + end + end + + context 'when [ci skip] should be ignored' do + let(:command) do + double('command', project: project, + current_user: user, + ignore_skip_ci: true) + end + + it 'does not break the chain' do + step.perform! + + expect(step.break?).to be false + end + end + + context 'when pipeline should be skipped but not persisted' do + let(:command) do + double('command', project: project, + current_user: user, + ignore_skip_ci: false, + save_incompleted: false) + end + + before do + allow(pipeline).to receive(:git_commit_message) + .and_return('commit message [ci skip]') + + step.perform! + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'does not skip pipeline' do + expect(pipeline.reload).not_to be_skipped + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb new file mode 100644 index 00000000000..0bbdd23f4d6 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: ref, project: project) + end + + let(:command) do + double('command', project: project, current_user: user) + end + + let(:step) { described_class.new(pipeline, command) } + + let(:ref) { 'master' } + + context 'when users has no ability to run a pipeline' do + before do + step.perform! + end + + it 'adds an error about insufficient permissions' do + expect(pipeline.errors.to_a) + .to include /Insufficient permissions/ + end + + it 'breaks the pipeline builder chain' do + expect(step.break?).to eq true + end + end + + context 'when user has ability to create a pipeline' do + before do + project.add_developer(user) + + step.perform! + end + + it 'does not invalidate the pipeline' do + expect(pipeline).to be_valid + end + + it 'does not break the chain' do + expect(step.break?).to eq false + end + end + + describe '#allowed_to_create?' do + subject { step.allowed_to_create? } + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to merge' do + let!(:protected_branch) do + create(:protected_branch, + :developers_can_merge, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :developers_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_truthy } + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_truthy } + + context 'when no one can create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :no_one_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_falsey } + end + end + end + + context 'when owner cannot create pipeline' do + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb new file mode 100644 index 00000000000..3740df88f42 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Validate::Config do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:command) do + double('command', project: project, + current_user: user, + save_incompleted: true) + end + + let!(:step) { described_class.new(pipeline, command) } + + before do + step.perform! + end + + context 'when pipeline has no YAML configuration' do + let(:pipeline) do + build_stubbed(:ci_pipeline, project: project) + end + + it 'appends errors about missing configuration' do + expect(pipeline.errors.to_a) + .to include 'Missing .gitlab-ci.yml file' + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + end + + context 'when YAML configuration contains errors' do + let(:pipeline) do + build(:ci_pipeline, project: project, config: 'invalid YAML') + end + + it 'appends errors about YAML errors' do + expect(pipeline.errors.to_a) + .to include 'Invalid configuration format' + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + context 'when saving incomplete pipeline is allowed' do + let(:command) do + double('command', project: project, + current_user: user, + save_incompleted: true) + end + + it 'fails the pipeline' do + expect(pipeline.reload).to be_failed + end + end + + context 'when saving incomplete pipeline is not allowed' do + let(:command) do + double('command', project: project, + current_user: user, + save_incompleted: false) + end + + it 'does not drop pipeline' do + expect(pipeline).not_to be_failed + expect(pipeline).not_to be_persisted + end + end + end + + context 'when pipeline has no stages / jobs' do + let(:config) do + { rspec: { + script: 'ls', + only: ['something'] + } } + end + + let(:pipeline) do + build(:ci_pipeline, project: project, config: config) + end + + it 'appends an error about missing stages' do + expect(pipeline.errors.to_a) + .to include 'No stages / jobs for this pipeline.' + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + end + + context 'when pipeline contains configuration validation errors' do + let(:config) { { rspec: {} } } + + let(:pipeline) do + build(:ci_pipeline, project: project, config: config) + end + + it 'appends configuration validation errors to pipeline errors' do + expect(pipeline.errors.to_a) + .to include "jobs:rspec config can't be blank" + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + end + + context 'when pipeline is correct and complete' do + let(:pipeline) do + build(:ci_pipeline_with_one_job, project: project) + end + + it 'does not invalidate the pipeline' do + expect(pipeline).to be_valid + end + + it 'does not break the chain' do + expect(step.break?).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb new file mode 100644 index 00000000000..bb356efe9ad --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + let(:command) do + double('command', project: project, current_user: user) + end + + let!(:step) { described_class.new(pipeline, command) } + + before do + step.perform! + end + + context 'when pipeline ref and sha exists' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project) + end + + it 'does not break the chain' do + expect(step.break?).to be false + end + + it 'does not append pipeline errors' do + expect(pipeline.errors).to be_empty + end + end + + context 'when pipeline ref does not exist' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'something', project: project) + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'adds an error about missing ref' do + expect(pipeline.errors.to_a) + .to include 'Reference not found' + end + end + + context 'when pipeline does not have SHA set' do + let(:pipeline) do + build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project) + end + + it 'breaks the chain' do + expect(step.break?).to be true + end + + it 'adds an error about missing SHA' do + expect(pipeline.errors.to_a) + .to include 'Commit not found' + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb index b26728a843c..7c9836e2da6 100644 --- a/spec/lib/gitlab/ci/pipeline_duration_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::PipelineDuration do +describe Gitlab::Ci::Pipeline::Duration do let(:calculated_duration) { calculate(data) } shared_examples 'calculating duration' do @@ -107,9 +107,9 @@ describe Gitlab::Ci::PipelineDuration do def calculate(data) periods = data.shuffle.map do |(first, last)| - Gitlab::Ci::PipelineDuration::Period.new(first, last) + described_class::Period.new(first, last) end - Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first)) + described_class.from_periods(periods.sort_by(&:first)) end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 2278230f338..d72f8553f55 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -3,8 +3,7 @@ require 'spec_helper' module Gitlab module Ci describe YamlProcessor, :lib do - subject { described_class.new(config, path) } - let(:path) { 'path' } + subject { described_class.new(config) } describe 'our current .gitlab-ci.yml' do let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } @@ -17,7 +16,7 @@ module Gitlab end describe '#build_attributes' do - subject { described_class.new(config, path).build_attributes(:rspec) } + subject { described_class.new(config).build_attributes(:rspec) } describe 'coverage entry' do describe 'code coverage regexp' do @@ -167,8 +166,6 @@ module Gitlab end context 'when kubernetes policy is specified' do - let(:pipeline) { create(:ci_empty_pipeline) } - let(:config) do YAML.dump( spinach: { stage: 'test', script: 'spinach' }, @@ -204,7 +201,7 @@ module Gitlab end end - describe "#builds_for_stage_and_ref" do + describe "#pipeline_stage_builds" do let(:type) { 'test' } it "returns builds if no branch specified" do @@ -213,10 +210,10 @@ module Gitlab rspec: { script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).first).to eq({ stage: "test", stage_idx: 1, name: "rspec", @@ -241,9 +238,9 @@ module Gitlab rspec: { script: "rspec", only: ["deploy"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0) end it "does not return builds if only has regexp with another branch" do @@ -252,9 +249,9 @@ module Gitlab rspec: { script: "rspec", only: ["/^deploy$/"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0) end it "returns builds if only has specified this branch" do @@ -263,9 +260,9 @@ module Gitlab rspec: { script: "rspec", only: ["master"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) end it "returns builds if only has a list of branches including specified" do @@ -274,9 +271,9 @@ module Gitlab rspec: { script: "rspec", type: type, only: %w(master deploy) } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) end it "returns builds if only has a branches keyword specified" do @@ -285,9 +282,9 @@ module Gitlab rspec: { script: "rspec", type: type, only: ["branches"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) end it "does not return builds if only has a tags keyword" do @@ -296,9 +293,9 @@ module Gitlab rspec: { script: "rspec", type: type, only: ["tags"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) end it "returns builds if only has special keywords specified and source matches" do @@ -315,9 +312,9 @@ module Gitlab rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1) end end @@ -335,21 +332,27 @@ module Gitlab rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0) end end it "returns builds if only has current repository path" do + seed_pipeline = pipeline(ref: 'deploy') + config = YAML.dump({ before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@path"] } + rspec: { + script: "rspec", + type: type, + only: ["branches@#{seed_pipeline.project_full_path}"] + } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(1) end it "does not return builds if only has different repository path" do @@ -358,9 +361,9 @@ module Gitlab rspec: { script: "rspec", type: type, only: ["branches@fork"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) end it "returns build only for specified type" do @@ -371,11 +374,11 @@ module Gitlab production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork') + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) + expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "deploy")).size).to eq(2) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "deploy")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "master")).size).to eq(1) end context 'for invalid value' do @@ -418,9 +421,9 @@ module Gitlab rspec: { script: "rspec", except: ["deploy"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) end it "returns builds if except has regexp with another branch" do @@ -429,9 +432,9 @@ module Gitlab rspec: { script: "rspec", except: ["/^deploy$/"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) end it "does not return builds if except has specified this branch" do @@ -440,9 +443,9 @@ module Gitlab rspec: { script: "rspec", except: ["master"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0) end it "does not return builds if except has a list of branches including specified" do @@ -451,9 +454,9 @@ module Gitlab rspec: { script: "rspec", type: type, except: %w(master deploy) } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) end it "does not return builds if except has a branches keyword specified" do @@ -462,9 +465,9 @@ module Gitlab rspec: { script: "rspec", type: type, except: ["branches"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) end it "returns builds if except has a tags keyword" do @@ -473,9 +476,9 @@ module Gitlab rspec: { script: "rspec", type: type, except: ["tags"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) end it "does not return builds if except has special keywords specified and source matches" do @@ -492,9 +495,9 @@ module Gitlab rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0) end end @@ -512,21 +515,27 @@ module Gitlab rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1) end end it "does not return builds if except has current repository path" do + seed_pipeline = pipeline(ref: 'deploy') + config = YAML.dump({ before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@path"] } + rspec: { + script: "rspec", + type: type, + except: ["branches@#{seed_pipeline.project_full_path}"] + } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(0) end it "returns builds if except has different repository path" do @@ -535,24 +544,28 @@ module Gitlab rspec: { script: "rspec", type: type, except: ["branches@fork"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1) end it "returns build except specified type" do + master_pipeline = pipeline(ref: 'master') + test_pipeline = pipeline(ref: 'test') + deploy_pipeline = pipeline(ref: 'deploy') + config = YAML.dump({ before_script: ["pwd"], - rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, + rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@#{test_pipeline.project_full_path}"] }, staging: { script: "deploy", type: "deploy", except: ["master"] }, - production: { script: "deploy", type: "deploy", except: ["master@fork"] } + production: { script: "deploy", type: "deploy", except: ["master@#{master_pipeline.project_full_path}"] } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork') + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) + expect(config_processor.pipeline_stage_builds("deploy", deploy_pipeline).size).to eq(2) + expect(config_processor.pipeline_stage_builds("test", test_pipeline).size).to eq(0) + expect(config_processor.pipeline_stage_builds("deploy", master_pipeline).size).to eq(0) end context 'for invalid value' do @@ -591,9 +604,9 @@ module Gitlab describe "Scripts handling" do let(:config_data) { YAML.dump(config) } - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data, path) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) } - subject { config_processor.builds_for_stage_and_ref("test", "master").first } + subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first } describe "before_script" do context "in global context" do @@ -674,10 +687,10 @@ module Gitlab before_script: ["pwd"], rspec: { script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ stage: "test", stage_idx: 1, name: "rspec", @@ -709,10 +722,10 @@ module Gitlab command: ["/usr/local/bin/init", "run"] }, "docker:dind"], script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ stage: "test", stage_idx: 1, name: "rspec", @@ -742,10 +755,10 @@ module Gitlab before_script: ["pwd"], rspec: { script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ stage: "test", stage_idx: 1, name: "rspec", @@ -771,10 +784,10 @@ module Gitlab before_script: ["pwd"], rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ stage: "test", stage_idx: 1, name: "rspec", @@ -797,7 +810,7 @@ module Gitlab end describe 'Variables' do - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), path) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } subject { config_processor.builds.first[:yaml_variables] } @@ -918,9 +931,9 @@ module Gitlab rspec: { script: "rspec", when: when_state } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - builds = config_processor.builds_for_stage_and_ref("test", "master") + builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) expect(builds.size).to eq(1) expect(builds.first[:when]).to eq(when_state) end @@ -951,8 +964,8 @@ module Gitlab config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, key: 'key', @@ -970,8 +983,8 @@ module Gitlab config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, key: 'key', @@ -990,8 +1003,8 @@ module Gitlab config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( paths: ["test/"], untracked: false, key: 'local', @@ -1019,8 +1032,8 @@ module Gitlab config_processor = Gitlab::Ci::YamlProcessor.new(config) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) + expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ stage: "test", stage_idx: 1, name: "rspec", @@ -1055,9 +1068,9 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + config_processor = Gitlab::Ci::YamlProcessor.new(config) - builds = config_processor.builds_for_stage_and_ref("test", "master") + builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) expect(builds.size).to eq(1) expect(builds.first[:options][:artifacts][:when]).to eq(when_state) end @@ -1072,7 +1085,7 @@ module Gitlab end let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } - let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') } + let(:builds) { processor.pipeline_stage_builds('deploy', pipeline(ref: 'master')) } context 'when a production environment is specified' do let(:environment) { 'production' } @@ -1229,7 +1242,7 @@ module Gitlab describe "Hidden jobs" do let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("test", "master") } + subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) } shared_examples 'hidden_job_handling' do it "doesn't create jobs that start with dot" do @@ -1277,7 +1290,7 @@ module Gitlab describe "YAML Alias/Anchor" do let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("build", "master") } + subject { config_processor.pipeline_stage_builds("build", pipeline(ref: "master")) } shared_examples 'job_templates_handling' do it "is correctly supported for jobs" do @@ -1377,182 +1390,182 @@ EOT it "returns errors if tags parameter is invalid" do config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") end it "returns errors if before_script parameter is invalid" do config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings") end it "returns errors if job before_script parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") end it "returns errors if after_script parameter is invalid" do config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings") end it "returns errors if job after_script parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") end it "returns errors if image parameter is invalid" do config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string") end it "returns errors if job name is blank" do config = YAML.dump({ '' => { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank") end it "returns errors if job name is non-string" do config = YAML.dump({ 10 => { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol") end it "returns errors if job image parameter is invalid" do config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") end it "returns errors if services parameter is not an array" do config = YAML.dump({ services: "test", rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array") end it "returns errors if services parameter is not an array of strings" do config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") end it "returns errors if job services parameter is not an array" do config = YAML.dump({ rspec: { script: "test", services: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array") end it "returns errors if job services parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") end it "returns error if job configuration is invalid" do config = YAML.dump({ extra: "bundle update" }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash") end it "returns errors if services configuration is not correct" do config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array") end it "returns errors if there are no jobs defined" do config = YAML.dump({ before_script: ["bundle update"] }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") end it "returns errors if there are no visible jobs defined" do config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") end it "returns errors if job allow_failure parameter is not an boolean" do config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") end it "returns errors if job stage is not a string" do config = YAML.dump({ rspec: { script: "test", type: 1 } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string") end it "returns errors if job stage is not a pre-defined stage" do config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") end it "returns errors if job stage is not a defined stage" do config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") end it "returns errors if stages is not an array" do config = YAML.dump({ stages: "test", rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") end it "returns errors if stages is not an array of strings" do config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") end it "returns errors if variables is not a map" do config = YAML.dump({ variables: "test", rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") end it "returns errors if variables is not a map of key-value strings" do config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") end it "returns errors if job when is not on_success, on_failure or always" do config = YAML.dump({ rspec: { script: "test", when: 1 } }) expect do - Gitlab::Ci::YamlProcessor.new(config, path) + Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") end @@ -1694,6 +1707,10 @@ EOT end end end + + def pipeline(**attributes) + build_stubbed(:ci_empty_pipeline, **attributes) + end end end end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 15012495247..9e528392756 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -347,10 +347,10 @@ describe Gitlab::ClosingIssueExtractor do end it "fetches cross-project URL references" do - message = "Closes #{urls.project_issue_url(issue2.project, issue2)} and #{reference}" + message = "Closes #{urls.project_issue_url(issue2.project, issue2)}, #{reference} and #{urls.project_issue_url(other_issue.project, other_issue)}" expect(subject.closed_by_message(message)) - .to match_array([issue, issue2]) + .to match_array([issue, issue2, other_issue]) end it "ignores invalid cross-project URL references" do diff --git a/spec/lib/gitlab/diff/diff_refs_spec.rb b/spec/lib/gitlab/diff/diff_refs_spec.rb index c73708d90a8..f9bfb4c469e 100644 --- a/spec/lib/gitlab/diff/diff_refs_spec.rb +++ b/spec/lib/gitlab/diff/diff_refs_spec.rb @@ -3,6 +3,61 @@ require 'spec_helper' describe Gitlab::Diff::DiffRefs do let(:project) { create(:project, :repository) } + describe '#==' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + subject { commit.diff_refs } + + context 'when shas are missing' do + let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: nil) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + + context 'when shas are equal' do + let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: subject.head_sha) } + + it 'returns true' do + expect(subject).to eq(other) + end + end + + context 'when shas are unequal' do + let(:other) { described_class.new(base_sha: subject.base_sha, start_sha: subject.start_sha, head_sha: subject.head_sha.reverse) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + + context 'when shas are truncated' do + context 'when sha prefixes are too short' do + let(:other) { described_class.new(base_sha: subject.base_sha[0, 4], start_sha: subject.start_sha[0, 4], head_sha: subject.head_sha[0, 4]) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + + context 'when sha prefixes are equal' do + let(:other) { described_class.new(base_sha: subject.base_sha[0, 10], start_sha: subject.start_sha[0, 10], head_sha: subject.head_sha[0, 10]) } + + it 'returns true' do + expect(subject).to eq(other) + end + end + + context 'when sha prefixes are unequal' do + let(:other) { described_class.new(base_sha: subject.base_sha[0, 10], start_sha: subject.start_sha[0, 10], head_sha: subject.head_sha[0, 10].reverse) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + end + end + describe '#compare_in' do context 'with diff refs for the initial commit' do let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index d4a2a852c12..7798736a4dc 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -429,6 +429,44 @@ describe Gitlab::Diff::Position do end end + describe '#==' do + let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } + + subject do + described_class.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + diff_refs: commit.diff_refs + ) + end + + context 'when positions are equal' do + let(:other) { described_class.new(subject.to_h) } + + it 'returns true' do + expect(subject).to eq(other) + end + end + + context 'when positions are equal, except for truncated shas' do + let(:other) { described_class.new(subject.to_h.merge(start_sha: subject.start_sha[0, 10])) } + + it 'returns true' do + expect(subject).to eq(other) + end + end + + context 'when positions are unequal' do + let(:other) { described_class.new(subject.to_h.merge(start_sha: subject.start_sha.reverse)) } + + it 'returns false' do + expect(subject).not_to eq(other) + end + end + end + describe "#to_json" do let(:hash) do { diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index a3d323fe28a..7dc06c90078 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -1,11 +1,14 @@ require 'spec_helper' describe Gitlab::Gfm::ReferenceRewriter do - let(:text) { 'some text' } - let(:old_project) { create(:project, name: 'old-project') } - let(:new_project) { create(:project, name: 'new-project') } + let(:group) { create(:group) } + let(:old_project) { create(:project, name: 'old-project', group: group) } + let(:new_project) { create(:project, name: 'new-project', group: group) } let(:user) { create(:user) } + let(:old_project_ref) { old_project.to_reference(new_project) } + let(:text) { 'some text' } + before do old_project.team << [user, :reporter] end @@ -39,7 +42,7 @@ describe Gitlab::Gfm::ReferenceRewriter do it { is_expected.not_to include merge_request.to_reference(new_project) } end - context 'description ambigous elements' do + context 'rewrite ambigous references' do context 'url' do let(:url) { 'http://gitlab.com/#1' } let(:text) { "This references #1, but not #{url}" } @@ -66,23 +69,21 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with project labels' do let!(:label) { create(:label, id: 123, name: 'test', project: old_project) } - let(:project_ref) { old_project.to_reference(new_project) } context 'label referenced by id' do let(:text) { '#1 and ~123' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} } end context 'label referenced by text' do let(:text) { '#1 and ~"test"' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} } end end context 'description with group labels' do let(:old_group) { create(:group) } let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) } - let(:project_ref) { old_project.to_reference(new_project) } before do old_project.update(namespace: old_group) @@ -90,21 +91,53 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'label referenced by id' do let(:text) { '#1 and ~321' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} } end context 'label referenced by text' do let(:text) { '#1 and ~"group label"' } - it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} } + it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} } end end end + end + + context 'reference contains project milestone' do + let!(:milestone) do + create(:milestone, title: '9.0', project: old_project) + end + + let(:text) { 'milestone: %"9.0"' } + + it { is_expected.to eq %Q[milestone: #{old_project_ref}%"9.0"] } + end + + context 'when referring to group milestone' do + let!(:milestone) do + create(:milestone, title: '10.0', group: group) + end + + let(:text) { 'milestone %"10.0"' } + + it { is_expected.to eq text } + end + + context 'when referable has a nil reference' do + before do + create(:milestone, title: '9.0', project: old_project) + + allow_any_instance_of(Milestone) + .to receive(:to_reference) + .and_return(nil) + end - context 'reference contains milestone' do - let(:milestone) { create(:milestone) } - let(:text) { "milestone ref: #{milestone.to_reference}" } + let(:text) { 'milestone: %"9.0"' } - it { is_expected.to eq text } + it 'raises an error that should be fixed' do + expect { subject }.to raise_error( + described_class::RewriteError, + 'Unspecified reference detected for Milestone' + ) end end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 66ba00acb7d..f3945e748ab 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -119,10 +119,13 @@ describe Gitlab::Git::Blob, seed_helper: true do shared_examples 'finding blobs by ID' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } + let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) } + it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(raw_blob.data[0..10]).to eq("require \'fi") } it { expect(raw_blob.size).to eq(669) } it { expect(raw_blob.truncated?).to be_falsey } + it { expect(bad_blob).to be_nil } context 'large file' do it 'limits the size of a large file' do diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 46e968cc398..a3dff6d0d4b 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -181,7 +181,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '.where' do + shared_examples '.where' do context 'path is empty string' do subject do commits = described_class.where( @@ -279,6 +279,14 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe '.where with gitaly' do + it_should_behave_like '.where' + end + + describe '.where without gitaly', skip_gitaly_mock: true do + it_should_behave_like '.where' + end + describe '.between' do subject do commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index ea3e4680b1d..0ff4f3bd105 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -28,6 +28,7 @@ describe Gitlab::Git::Hook do f.write(<<-HOOK) echo 'regular message from the hook' echo 'error message from the hook' 1>&2 + echo 'error message from the hook line 2' 1>&2 exit 1 HOOK end @@ -73,7 +74,7 @@ describe Gitlab::Git::Hook do status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be false - expect(errors).to eq("error message from the hook\n") + expect(errors).to eq("error message from the hook<br>error message from the hook line 2<br>") end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 556a148c3bc..a0482e30a33 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -389,6 +389,40 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#has_local_branches?' do + shared_examples 'check for local branches' do + it { expect(repository.has_local_branches?).to eq(true) } + + context 'mutable' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + after do + ensure_seeds + end + + it 'returns false when there are no branches' do + # Sanity check + expect(repository.has_local_branches?).to eq(true) + + FileUtils.rm_rf(File.join(repository.path, 'packed-refs')) + heads_dir = File.join(repository.path, 'refs/heads') + FileUtils.rm_rf(heads_dir) + FileUtils.mkdir_p(heads_dir) + + expect(repository.has_local_branches?).to eq(false) + end + end + end + + context 'with gitaly' do + it_behaves_like 'check for local branches' + end + + context 'without gitaly', skip_gitaly_mock: true do + it_behaves_like 'check for local branches' + end + end + describe "#delete_branch" do shared_examples "deleting a branch" do let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } @@ -481,7 +515,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'raises an error if it failed' do - expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1]) + expect(@repo).to receive(:popen).and_return(['Error', 1]) expect do @repo.delete_refs('refs/heads/fix') @@ -1332,6 +1366,84 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#with_repo_branch_commit' do + context 'when comparing with the same repository' do + let(:start_repository) { repository } + + context 'when the branch exists' do + let(:start_branch_name) { 'master' } + + it 'yields the commit' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Commit)) + end + end + + context 'when the branch does not exist' do + let(:start_branch_name) { 'definitely-not-master' } + + it 'yields nil' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(nil) + end + end + end + + context 'when comparing with another repository' do + let(:start_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + context 'when the branch exists' do + let(:start_branch_name) { 'master' } + + it 'yields the commit' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Commit)) + end + end + + context 'when the branch does not exist' do + let(:start_branch_name) { 'definitely-not-master' } + + it 'yields nil' do + expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } + .to yield_with_args(nil) + end + end + end + end + + describe '#fetch_source_branch' do + let(:local_ref) { 'refs/merge-requests/1/head' } + + context 'when the branch exists' do + let(:source_branch) { 'master' } + + it 'writes the ref' do + expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/) + + repository.fetch_source_branch(repository, source_branch, local_ref) + end + + it 'returns true' do + expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(true) + end + end + + context 'when the branch does not exist' do + let(:source_branch) { 'definitely-not-master' } + + it 'does not write the ref' do + expect(repository).not_to receive(:write_ref) + + repository.fetch_source_branch(repository, source_branch, local_ref) + end + + it 'returns false' do + expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(false) + end + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index b051a088171..c0eac98d718 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Git::RevList do let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } it 'calls out to `popen`' do - expect(Gitlab::Popen).to receive(:popen).with([ + expect(rev_list).to receive(:popen).with([ Gitlab.config.git.bin_path, "--git-dir=#{project.repository.path_to_repo}", 'rev-list', @@ -36,7 +36,7 @@ describe Gitlab::Git::RevList do let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } it 'calls out to `popen`' do - expect(Gitlab::Popen).to receive(:popen).with([ + expect(rev_list).to receive(:popen).with([ Gitlab.config.git.bin_path, "--git-dir=#{project.repository.path_to_repo}", 'rev-list', diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index c86353abb7c..98cf7966dad 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do let(:storage_name) { 'default' } - let(:circuit_breaker) { described_class.new(storage_name) } + let(:circuit_breaker) { described_class.new(storage_name, hostname) } let(:hostname) { Gitlab::Environment.hostname } let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } @@ -22,7 +22,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: 'failure_wait_time' => 30, 'failure_reset_time' => 1800, 'storage_timeout' => 5 - } + }, + 'nopath' => { 'path' => nil } ) end @@ -59,6 +60,14 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(breaker).to be_a(described_class) expect(described_class.for_storage('default')).to eq(breaker) end + + it 'returns a broken circuit breaker for an unknown storage' do + expect(described_class.for_storage('unknown').circuit_broken?).to be_truthy + end + + it 'returns a broken circuit breaker when the path is not set' do + expect(described_class.for_storage('nopath').circuit_broken?).to be_truthy + end end describe '#initialize' do diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb new file mode 100644 index 00000000000..0e645008c88 --- /dev/null +++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::NullCircuitBreaker do + let(:storage) { 'default' } + let(:hostname) { 'localhost' } + let(:error) { nil } + + subject(:breaker) { described_class.new(storage, hostname, error: error) } + + context 'with an error' do + let(:error) { Gitlab::Git::Storage::Misconfiguration.new('error') } + + describe '#perform' do + it { expect { breaker.perform { 'ok' } }.to raise_error(error) } + end + + describe '#circuit_broken?' do + it { expect(breaker.circuit_broken?).to be_truthy } + end + + describe '#last_failure' do + it { Timecop.freeze { expect(breaker.last_failure).to eq(Time.now) } } + end + + describe '#failure_count' do + it { expect(breaker.failure_count).to eq(breaker.failure_count_threshold) } + end + + describe '#failure_info' do + it { Timecop.freeze { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(Time.now, breaker.failure_count_threshold)) } } + end + end + + context 'not broken' do + describe '#perform' do + it { expect(breaker.perform { 'ok' }).to eq('ok') } + end + + describe '#circuit_broken?' do + it { expect(breaker.circuit_broken?).to be_falsy } + end + + describe '#last_failure' do + it { expect(breaker.last_failure).to be_nil } + end + + describe '#failure_count' do + it { expect(breaker.failure_count).to eq(0) } + end + + describe '#failure_info' do + it { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(nil, 0)) } + end + end + + describe '#failure_count_threshold' do + it { expect(breaker.failure_count_threshold).to eq(1) } + end + + it 'implements the CircuitBreaker interface' do + ours = described_class.public_instance_methods + theirs = Gitlab::Git::Storage::CircuitBreaker.public_instance_methods + + # These methods are not part of the public API, but are public to allow the + # CircuitBreaker specs to operate. They should be made private over time. + exceptions = %i[ + cache_key + check_storage_accessible! + no_failures? + storage_available? + track_storage_accessible + track_storage_inaccessible + ] + + expect(theirs - ours).to contain_exactly(*exceptions) + end +end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index ec3abcb0953..1ef3e2e3a5d 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -51,6 +51,10 @@ describe Gitlab::GitalyClient::CommitService do expect(ret).to be_kind_of(Gitlab::GitalyClient::DiffStitcher) end + + it 'encodes paths correctly' do + expect { client.diff_from_parent(commit, paths: ['encoding/test.txt', 'encoding/テスト.txt']) }.not_to raise_error + end end describe '#commit_deltas' do diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index a9b861fcff2..9a84d6e6a67 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -38,6 +38,130 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do end end + describe 'allow_n_plus_1_calls' do + context 'when RequestStore is enabled', :request_store do + it 'returns the result of the allow_n_plus_1_calls block' do + expect(described_class.allow_n_plus_1_calls { "result" }).to eq("result") + end + end + + context 'when RequestStore is not active' do + it 'returns the result of the allow_n_plus_1_calls block' do + expect(described_class.allow_n_plus_1_calls { "something" }).to eq("something") + end + end + end + + describe 'enforce_gitaly_request_limits?' do + def call_gitaly(count = 1) + (1..count).each do + described_class.enforce_gitaly_request_limits(:test) + end + end + + context 'when RequestStore is enabled', :request_store do + it 'allows up the maximum number of allowed calls' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error + end + + context 'when the maximum number of calls has been reached' do + before do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) + end + + it 'fails on the next call' do + expect { call_gitaly(1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError) + end + end + + it 'allows the maximum number of calls to be exceeded within an allow_n_plus_1_calls block' do + expect do + described_class.allow_n_plus_1_calls do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) + end + end.not_to raise_error + end + + context 'when the maximum number of calls has been reached within an allow_n_plus_1_calls block' do + before do + described_class.allow_n_plus_1_calls do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) + end + end + + it 'allows up to the maximum number of calls outside of an allow_n_plus_1_calls block' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error + end + + it 'does not allow the maximum number of calls to be exceeded outside of an allow_n_plus_1_calls block' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError) + end + end + end + + context 'when RequestStore is not active' do + it 'does not raise errors when the maximum number of allowed calls is exceeded' do + expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 2) }.not_to raise_error + end + + it 'does not fail when the maximum number of calls is exceeded within an allow_n_plus_1_calls block' do + expect do + described_class.allow_n_plus_1_calls do + call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) + end + end.not_to raise_error + end + end + end + + describe 'get_request_count' do + context 'when RequestStore is enabled', :request_store do + context 'when enforce_gitaly_request_limits is called outside of allow_n_plus_1_calls blocks' do + before do + described_class.enforce_gitaly_request_limits(:call) + end + + it 'counts gitaly calls' do + expect(described_class.get_request_count).to eq(1) + end + end + + context 'when enforce_gitaly_request_limits is called inside and outside of allow_n_plus_1_calls blocks' do + before do + described_class.enforce_gitaly_request_limits(:call) + described_class.allow_n_plus_1_calls do + described_class.enforce_gitaly_request_limits(:call) + end + end + + it 'counts gitaly calls' do + expect(described_class.get_request_count).to eq(2) + end + end + + context 'when reset_counts is called' do + before do + described_class.enforce_gitaly_request_limits(:call) + described_class.reset_counts + end + + it 'resets counts' do + expect(described_class.get_request_count).to eq(0) + end + end + end + + context 'when RequestStore is not active' do + before do + described_class.enforce_gitaly_request_limits(:call) + end + + it 'returns zero' do + expect(described_class.get_request_count).to eq(0) + end + end + end + describe 'feature_enabled?' do let(:feature_name) { 'my_feature' } let(:real_feature_name) { "gitaly_#{feature_name}" } diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index f5c9680bf59..73dd236a5c6 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do let(:metric_class) { Gitlab::HealthChecks::Metric } let(:result_class) { Gitlab::HealthChecks::Result } - let(:repository_storages) { [:default] } + let(:repository_storages) { ['default'] } let(:tmp_dir) { Dir.mktmpdir } let(:storages_paths) do @@ -64,7 +64,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do allow(described_class).to receive(:storage_circuitbreaker_test) { true } end - it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) } + it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) } end context 'storage points to directory that has both read and write rights' do @@ -72,7 +72,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do FileUtils.chmod_R(0755, tmp_dir) end - it { is_expected.to include(result_class.new(true, nil, shard: :default)) } + it { is_expected.to include(result_class.new(true, nil, shard: 'default')) } it 'cleans up files used for testing' do expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original @@ -85,7 +85,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do allow(described_class).to receive(:storage_read_test).with(any_args).and_return(false) end - it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: :default)) } + it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: 'default')) } end context 'write test fails' do @@ -93,7 +93,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do allow(described_class).to receive(:storage_write_test).with(any_args).and_return(false) end - it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: :default)) } + it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: 'default')) } end end end @@ -109,7 +109,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do it 'provides metrics' do metrics = described_class.metrics - expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to all(have_attributes(labels: { shard: 'default' })) expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) @@ -128,7 +128,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do it 'provides metrics' do metrics = described_class.metrics - expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to all(have_attributes(labels: { shard: 'default' })) expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) @@ -156,14 +156,14 @@ describe Gitlab::HealthChecks::FsShardsCheck do describe '#readiness' do subject { described_class.readiness } - it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) } + it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) } end describe '#metrics' do it 'provides metrics' do metrics = described_class.metrics - expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to all(have_attributes(labels: { shard: 'default' })) expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index c7930378240..be11647415e 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -48,14 +48,35 @@ describe Gitlab::Shell do end end - describe '#add_key' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + describe 'projects commands' do + let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') } + let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') } + let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') } + + before do + allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path) + allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path) + allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) + end + + describe '#mv_repository' do + it 'executes the command' do + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [projects_path, 'mv-project', 'storage/path', 'project/path.git', 'new/path.git'] + ) + gitlab_shell.mv_repository('storage/path', 'project/path', 'new/path') + end + end + + describe '#add_key' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end end end @@ -105,30 +126,42 @@ describe Gitlab::Shell do end describe '#add_repository' do - it 'creates a repository' do - created_path = File.join(TestEnv.repos_path, 'project', 'path.git') - hooks_path = File.join(created_path, 'hooks') + shared_examples '#add_repository' do + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } + let(:repo_name) { 'project/path' } + let(:created_path) { File.join(repository_storage_path, repo_name + '.git') } - begin - result = gitlab_shell.add_repository(TestEnv.repos_path, 'project/path') - - repo_stat = File.stat(created_path) rescue nil - hooks_stat = File.lstat(hooks_path) rescue nil - hooks_dir = File.realpath(hooks_path) - ensure + after do FileUtils.rm_rf(created_path) end - expect(result).to be_truthy - expect(repo_stat.mode & 0o777).to eq(0o770) - expect(hooks_stat.symlink?).to be_truthy - expect(hooks_dir).to eq(gitlab_shell_hooks_path) + it 'creates a repository' do + expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_truthy + + expect(File.stat(created_path).mode & 0o777).to eq(0o770) + + hooks_path = File.join(created_path, 'hooks') + expect(File.lstat(hooks_path)).to be_symlink + expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path) + end + + it 'returns false when the command fails' do + FileUtils.mkdir_p(File.dirname(created_path)) + # This file will block the creation of the repo's .git directory. That + # should cause #add_repository to fail. + FileUtils.touch(created_path) + + expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_falsy + end end - it 'returns false when the command fails' do - expect(FileUtils).to receive(:mkdir_p).and_raise(Errno::EEXIST) + context 'with gitlay' do + it_behaves_like '#add_repository' + end - expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be_falsy + context 'without gitaly', skip_gitaly_mock: true do + it_behaves_like '#add_repository' end end @@ -136,7 +169,7 @@ describe Gitlab::Shell do it 'returns true when the command succeeds' do expect(Gitlab::Popen).to receive(:popen) .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return([nil, 0]) + nil, popen_vars).and_return([nil, 0]) expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true end @@ -144,7 +177,7 @@ describe Gitlab::Shell do it 'returns false when the command fails' do expect(Gitlab::Popen).to receive(:popen) .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return(["error", 1]) + nil, popen_vars).and_return(["error", 1]) expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index c7d9f105f04..ee152872acc 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -26,6 +26,16 @@ describe Gitlab::UsageData do version uuid hostname + signup + ldap + gravatar + omniauth + reply_by_email + container_registry + gitlab_pages + gitlab_shared_runners + git + database )) end @@ -86,6 +96,32 @@ describe Gitlab::UsageData do end end + describe '#features_usage_data_ce' do + subject { described_class.features_usage_data_ce } + + it 'gathers feature usage data' do + expect(subject[:signup]).to eq(current_application_settings.signup_enabled?) + expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled) + expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?) + expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled) + expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?) + expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled) + expect(subject[:gitlab_shared_runners]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled) + end + end + + describe '#components_usage_data' do + subject { described_class.components_usage_data } + + it 'gathers components usage data' do + expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled) + expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION) + expect(subject[:git][:version]).to eq(Gitlab::Git.version) + expect(subject[:database][:adapter]).to eq(Gitlab::Database.adapter_name) + expect(subject[:database][:version]).to eq(Gitlab::Database.version) + end + end + describe '#license_usage_data' do subject { described_class.license_usage_data } diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 699184ad9fe..5708aa6754f 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -214,14 +214,12 @@ describe Gitlab::Workhorse do end it 'includes a Repository param' do - repo_param = { Repository: { + repo_param = { storage_name: 'default', - relative_path: project.full_path + '.git', - git_object_directory: '', - git_alternate_object_directories: [] - } } + relative_path: project.full_path + '.git' + } - expect(subject).to include(repo_param) + expect(subject[:Repository]).to include(repo_param) end context "when git_upload_pack action is passed" do diff --git a/spec/lib/system_check/base_check_spec.rb b/spec/lib/system_check/base_check_spec.rb new file mode 100644 index 00000000000..faf8c99e772 --- /dev/null +++ b/spec/lib/system_check/base_check_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe SystemCheck::BaseCheck do + context 'helpers on instance level' do + it 'responds to SystemCheck::Helpers methods' do + expect(subject).to respond_to :fix_and_rerun, :for_more_information, :see_installation_guide_section, + :finished_checking, :start_checking, :try_fixing_it, :sanitized_message, :should_sanitize?, :omnibus_gitlab?, + :sudo_gitlab + end + + it 'responds to Gitlab::TaskHelpers methods' do + expect(subject).to respond_to :ask_to_continue, :os_name, :prompt, :run_and_match, :run_command, + :run_command!, :uid_for, :gid_for, :gitlab_user, :gitlab_user?, :warn_user_is_not_gitlab, :all_repos, + :repository_storage_paths_args, :user_home, :checkout_or_clone_version, :clone_repo, :checkout_version + end + end +end diff --git a/spec/migrations/clean_stages_statuses_migration_spec.rb b/spec/migrations/clean_stages_statuses_migration_spec.rb new file mode 100644 index 00000000000..38705f8eaae --- /dev/null +++ b/spec/migrations/clean_stages_statuses_migration_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20170912113435_clean_stages_statuses_migration.rb') + +describe CleanStagesStatusesMigration, :migration, :sidekiq, :redis do + let(:migration) { spy('migration') } + + before do + allow(Gitlab::BackgroundMigration::MigrateStageStatus) + .to receive(:new).and_return(migration) + end + + context 'when there are pending background migrations' do + it 'processes pending jobs synchronously' do + Sidekiq::Testing.disable! do + BackgroundMigrationWorker + .perform_in(2.minutes, 'MigrateStageStatus', [1, 1]) + BackgroundMigrationWorker + .perform_async('MigrateStageStatus', [1, 1]) + + migrate! + + expect(migration).to have_received(:perform).with(1, 1).twice + end + end + end + + context 'when there are no background migrations pending' do + it 'does nothing' do + Sidekiq::Testing.disable! do + migrate! + + expect(migration).not_to have_received(:perform) + end + end + end + + context 'when there are still unmigrated stages afterwards' do + let(:stages) { table('ci_stages') } + + before do + stages.create!(status: nil, name: 'build') + stages.create!(status: nil, name: 'test') + end + + it 'migrates statuses sequentially in batches' do + migrate! + + expect(migration).to have_received(:perform).once + end + end +end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb new file mode 100644 index 00000000000..4ab1bb67058 --- /dev/null +++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170926150348_schedule_merge_request_diff_migrations_take_two') + +describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do + matcher :be_scheduled_migration do |time, *expected| + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + job['args'] == [migration, expected] && + job['at'].to_i == time.to_i + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" + end + end + + let(:merge_request_diffs) { table(:merge_request_diffs) } + let(:merge_requests) { table(:merge_requests) } + let(:projects) { table(:projects) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + + projects.create!(id: 1, name: 'gitlab', path: 'gitlab') + + merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master') + + merge_request_diffs.create!(id: 1, merge_request_id: 1, st_commits: YAML.dump([]), st_diffs: nil) + merge_request_diffs.create!(id: 2, merge_request_id: 1, st_commits: nil, st_diffs: YAML.dump([])) + merge_request_diffs.create!(id: 3, merge_request_id: 1, st_commits: nil, st_diffs: nil) + merge_request_diffs.create!(id: 4, merge_request_id: 1, st_commits: YAML.dump([]), st_diffs: YAML.dump([])) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes.from_now, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes.from_now, 4, 4) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'migrates the data' do + Sidekiq::Testing.inline! do + non_empty = 'st_commits IS NOT NULL OR st_diffs IS NOT NULL' + + expect(merge_request_diffs.where(non_empty).count).to eq 3 + + migrate! + + expect(merge_request_diffs.where(non_empty).count).to eq 0 + end + end +end diff --git a/spec/migrations/update_legacy_diff_notes_type_for_import_spec.rb b/spec/migrations/update_legacy_diff_notes_type_for_import_spec.rb new file mode 100644 index 00000000000..d625b60ff50 --- /dev/null +++ b/spec/migrations/update_legacy_diff_notes_type_for_import_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170927112318_update_legacy_diff_notes_type_for_import.rb') + +describe UpdateLegacyDiffNotesTypeForImport, :migration do + let(:notes) { table(:notes) } + + before do + notes.inheritance_column = nil + + notes.create(type: 'Note') + notes.create(type: 'LegacyDiffNote') + notes.create(type: 'Github::Import::Note') + notes.create(type: 'Github::Import::LegacyDiffNote') + end + + it 'updates the notes type' do + migrate! + + expect(notes.pluck(:type)) + .to contain_exactly('Note', 'Github::Import::Note', 'LegacyDiffNote', 'LegacyDiffNote') + end +end diff --git a/spec/migrations/update_notes_type_for_import_spec.rb b/spec/migrations/update_notes_type_for_import_spec.rb new file mode 100644 index 00000000000..06195d970d8 --- /dev/null +++ b/spec/migrations/update_notes_type_for_import_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170927112319_update_notes_type_for_import.rb') + +describe UpdateNotesTypeForImport, :migration do + let(:notes) { table(:notes) } + + before do + notes.inheritance_column = nil + + notes.create(type: 'Note') + notes.create(type: 'LegacyDiffNote') + notes.create(type: 'Github::Import::Note') + notes.create(type: 'Github::Import::LegacyDiffNote') + end + + it 'updates the notes type' do + migrate! + + expect(notes.pluck(:type)) + .to contain_exactly('Note', 'Note', 'LegacyDiffNote', 'Github::Import::LegacyDiffNote') + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 77f0be6b120..9c1e460ab20 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -26,6 +26,7 @@ describe Ci::Pipeline, :mailer do it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } + it { is_expected.to delegate_method(:full_path).to(:project).with_prefix } describe '#source' do context 'when creating new pipeline' do diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb index 2ce78e34b0c..889c243c8d8 100644 --- a/spec/models/ci/pipeline_variable_spec.rb +++ b/spec/models/ci/pipeline_variable_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::PipelineVariable, models: true do +describe Ci::PipelineVariable do subject { build(:ci_pipeline_variable) } it { is_expected.to include_module(HasVariable) } diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 2e686e515c5..584dfe9a5c1 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -183,75 +183,42 @@ describe Ci::Runner do end end - context 'when runner is locked' do + context 'when runner is shared' do before do - runner.locked = true + runner.is_shared = true + build.project.runners = [] end - shared_examples 'locked build picker' do - context 'when runner cannot pick untagged jobs' do - before do - runner.run_untagged = false - end + it 'can handle builds' do + expect(runner.can_pick?(build)).to be_truthy + end - it 'cannot handle builds without tags' do - expect(runner.can_pick?(build)).to be_falsey - end + context 'when runner is locked' do + before do + runner.locked = true end - context 'when having runner tags' do - before do - runner.tag_list = %w(bb cc) - end - - it 'cannot handle it for builds without matching tags' do - build.tag_list = ['aa'] - - expect(runner.can_pick?(build)).to be_falsey - end + it 'can handle builds' do + expect(runner.can_pick?(build)).to be_truthy end end + end - context 'when serving the same project' do - it 'can handle it' do + context 'when runner is not shared' do + context 'when runner is assigned to a project' do + it 'can handle builds' do expect(runner.can_pick?(build)).to be_truthy end - - it_behaves_like 'locked build picker' - - context 'when having runner tags' do - before do - runner.tag_list = %w(bb cc) - build.tag_list = ['bb'] - end - - it 'can handle it for matching tags' do - expect(runner.can_pick?(build)).to be_truthy - end - end end - context 'serving a different project' do + context 'when runner is not assigned to a project' do before do - runner.runner_projects.destroy_all + build.project.runners = [] end - it 'cannot handle it' do + it 'cannot handle builds' do expect(runner.can_pick?(build)).to be_falsey end - - it_behaves_like 'locked build picker' - - context 'when having runner tags' do - before do - runner.tag_list = %w(bb cc) - build.tag_list = ['bb'] - end - - it 'cannot handle it for matching tags' do - expect(runner.can_pick?(build)).to be_falsey - end - end end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index dbc4aba8547..8eabc4ca72f 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -37,30 +37,17 @@ describe Key, :mailer do end describe "#update_last_used_at" do - let(:key) { create(:key) } - - context 'when key was not updated during the last day' do - before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) - .and_return('000000') - end - - it 'enqueues a UseKeyWorker job' do - expect(UseKeyWorker).to receive(:perform_async).with(key.id) - key.update_last_used_at - end - end + it 'updates the last used timestamp' do + key = build(:key) + service = double(:service) + + expect(Keys::LastUsedService).to receive(:new) + .with(key) + .and_return(service) - context 'when key was updated during the last day' do - before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) - .and_return(false) - end + expect(service).to receive(:execute) - it 'does not enqueue a UseKeyWorker job' do - expect(UseKeyWorker).not_to receive(:perform_async) - key.update_last_used_at - end + key.update_last_used_at end end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index d3da0107d5c..13e37fffa4e 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -238,7 +238,7 @@ describe Milestone do let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') } it 'returns a String reference to the object' do - expect(milestone.to_reference).to eq '%1' + expect(milestone.to_reference).to eq '%"milestone"' end it 'returns a reference by name when the format is set to :name' do @@ -246,24 +246,29 @@ describe Milestone do end it 'supports a cross-project reference' do - expect(milestone.to_reference(another_project)).to eq 'sample-project%1' + expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"' end end context 'for a group milestone' do let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') } - it 'returns nil with the default format' do - expect(milestone.to_reference).to be_nil + it 'returns a group milestone reference with a default format' do + expect(milestone.to_reference).to eq '%"milestone"' end it 'returns a reference by name when the format is set to :name' do expect(milestone.to_reference(format: :name)).to eq '%"milestone"' end - it 'does not supports cross-project references' do + it 'does supports cross-project references within a group' do expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"' end + + it 'raises an error when using iid format' do + expect { milestone.to_reference(format: :iid) } + .to raise_error(ArgumentError, 'Cannot refer to a group milestone by an internal id!') + end end end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index ca13af4d73e..12069575866 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -8,7 +8,21 @@ describe ProjectAutoDevops do it { is_expected.to respond_to(:created_at) } it { is_expected.to respond_to(:updated_at) } - describe 'variables' do + describe '#has_domain?' do + context 'when domain is defined' do + let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: 'domain.com') } + + it { expect(auto_devops).to have_domain } + end + + context 'when domain is empty' do + let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: '') } + + it { expect(auto_devops).not_to have_domain } + end + end + + describe '#variables' do let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) } context 'when domain is defined' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 78226c6c3fa..868a843ab0a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1303,7 +1303,7 @@ describe Project do context 'using a regular repository' do it 'creates the repository' do expect(shell).to receive(:add_repository) - .with(project.repository_storage_path, project.disk_path) + .with(project.repository_storage, project.disk_path) .and_return(true) expect(project.repository).to receive(:after_create) @@ -1313,7 +1313,7 @@ describe Project do it 'adds an error if the repository could not be created' do expect(shell).to receive(:add_repository) - .with(project.repository_storage_path, project.disk_path) + .with(project.repository_storage, project.disk_path) .and_return(false) expect(project.repository).not_to receive(:after_create) @@ -1370,7 +1370,7 @@ describe Project do .and_return(false) expect(shell).to receive(:add_repository) - .with(project.repository_storage_path, project.disk_path) + .with(project.repository_storage, project.disk_path) .and_return(true) project.ensure_repository @@ -2363,12 +2363,24 @@ describe Project do describe '#legacy_storage?' do it 'returns true when storage_version is nil' do - project = build(:project) + project = build(:project, storage_version: nil) + + expect(project.legacy_storage?).to be_truthy + end + + it 'returns true when the storage_version is 0' do + project = build(:project, storage_version: 0) expect(project.legacy_storage?).to be_truthy end end + describe '#hashed_storage?' do + it 'returns false' do + expect(project.hashed_storage?).to be_falsey + end + end + describe '#rename_repo' do before do # Project#gitlab_shell returns a new instance of Gitlab::Shell on every @@ -2425,6 +2437,38 @@ describe Project do expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path)) end end + + describe '#migrate_to_hashed_storage!' do + it 'returns true' do + expect(project.migrate_to_hashed_storage!).to be_truthy + end + + it 'flags as readonly' do + expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true) + end + + it 'schedules ProjectMigrateHashedStorageWorker with delayed start when the project repo is in use' do + Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: false)).increase + + expect(ProjectMigrateHashedStorageWorker).to receive(:perform_in) + + project.migrate_to_hashed_storage! + end + + it 'schedules ProjectMigrateHashedStorageWorker with delayed start when the wiki repo is in use' do + Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: true)).increase + + expect(ProjectMigrateHashedStorageWorker).to receive(:perform_in) + + project.migrate_to_hashed_storage! + end + + it 'schedules ProjectMigrateHashedStorageWorker' do + expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).with(project.id) + + project.migrate_to_hashed_storage! + end + end end context 'hashed storage' do @@ -2438,6 +2482,18 @@ describe Project do allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) end + describe '#legacy_storage?' do + it 'returns false' do + expect(project.legacy_storage?).to be_falsey + end + end + + describe '#hashed_storage?' do + it 'returns true' do + expect(project.hashed_storage?).to be_truthy + end + end + describe '#base_dir' do it 'returns base_dir based on hash of project id' do expect(project.base_dir).to eq('@hashed/6b/86') @@ -2508,6 +2564,26 @@ describe Project do expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path)) end end + + describe '#migrate_to_hashed_storage!' do + it 'returns nil' do + expect(project.migrate_to_hashed_storage!).to be_nil + end + + it 'does not flag as readonly' do + expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only } + end + end + end + + describe '#gl_repository' do + let(:project) { create(:project) } + + it 'delegates to Gitlab::GlRepository.gl_repository' do + expect(Gitlab::GlRepository).to receive(:gl_repository).with(project, true) + + project.gl_repository(is_wiki: true) + end end describe '#has_ci?' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 60cd7e70055..ab81d39691b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Repository, models: true do +describe Repository do include RepoHelpers TestBlob = Struct.new(:path) @@ -1131,21 +1131,31 @@ describe Repository, models: true do end describe '#has_visible_content?' do - subject { repository.has_visible_content? } + before do + # If raw_repository.has_visible_content? gets called more than once then + # caching is broken. We don't want that. + expect(repository.raw_repository).to receive(:has_visible_content?) + .once + .and_return(result) + end - describe 'when there are no branches' do - before do - allow(repository.raw_repository).to receive(:branch_count).and_return(0) - end + context 'when true' do + let(:result) { true } - it { is_expected.to eq(false) } + it 'returns true and caches it' do + expect(repository.has_visible_content?).to eq(true) + # Second call hits the cache + expect(repository.has_visible_content?).to eq(true) + end end - describe 'when there are branches' do - it 'returns true' do - expect(repository.raw_repository).to receive(:branch_count).and_return(3) + context 'when false' do + let(:result) { false } - expect(subject).to eq(true) + it 'returns false and caches it' do + expect(repository.has_visible_content?).to eq(false) + # Second call hits the cache + expect(repository.has_visible_content?).to eq(false) end end end @@ -1311,24 +1321,25 @@ describe Repository, models: true do describe '#revert' do let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:message) { 'revert message' } context 'when there is a conflict' do it 'raises an error' do - expect { repository.revert(user, new_image_commit, 'master') }.to raise_error(/Failed to/) + expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end end context 'when commit was already reverted' do it 'raises an error' do - repository.revert(user, update_image_commit, 'master') + repository.revert(user, update_image_commit, 'master', message) - expect { repository.revert(user, update_image_commit, 'master') }.to raise_error(/Failed to/) + expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end end context 'when commit can be reverted' do it 'reverts the changes' do - expect(repository.revert(user, update_image_commit, 'master')).to be_truthy + expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy end end @@ -1337,7 +1348,7 @@ describe Repository, models: true do merge_commit expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present - repository.revert(user, merge_commit, 'master') + repository.revert(user, merge_commit, 'master', message) expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present end end @@ -1347,24 +1358,25 @@ describe Repository, models: true do let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') } let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') } + let(:message) { 'cherry-pick message' } context 'when there is a conflict' do it 'raises an error' do - expect { repository.cherry_pick(user, conflict_commit, 'master') }.to raise_error(/Failed to/) + expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end end context 'when commit was already cherry-picked' do it 'raises an error' do - repository.cherry_pick(user, pickable_commit, 'master') + repository.cherry_pick(user, pickable_commit, 'master', message) - expect { repository.cherry_pick(user, pickable_commit, 'master') }.to raise_error(/Failed to/) + expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end end context 'when commit can be cherry-picked' do it 'cherry-picks the changes' do - expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy + expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy end end @@ -1372,11 +1384,11 @@ describe Repository, models: true do it 'cherry-picks the changes' do expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil - cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome') + cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message) cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil - expect(cherry_pick_commit_message).to include('cherry picked from') + expect(cherry_pick_commit_message).to eq(message) end end end @@ -1615,27 +1627,41 @@ describe Repository, models: true do end describe '#add_tag' do - context 'with a valid target' do - let(:user) { build_stubbed(:user) } + let(:user) { build_stubbed(:user) } - it 'creates the tag using rugged' do - expect(repository.rugged.tags).to receive(:create) - .with('8.5', repository.commit('master').id, - hash_including(message: 'foo', - tagger: hash_including(name: user.name, email: user.email))) - .and_call_original + shared_examples 'adding tag' do + context 'with a valid target' do + it 'creates the tag' do + repository.add_tag(user, '8.5', 'master', 'foo') - repository.add_tag(user, '8.5', 'master', 'foo') - end + tag = repository.find_tag('8.5') + expect(tag).to be_present + expect(tag.message).to eq('foo') + expect(tag.dereferenced_target.id).to eq(repository.commit('master').id) + end - it 'returns a Gitlab::Git::Tag object' do - tag = repository.add_tag(user, '8.5', 'master', 'foo') + it 'returns a Gitlab::Git::Tag object' do + tag = repository.add_tag(user, '8.5', 'master', 'foo') - expect(tag).to be_a(Gitlab::Git::Tag) + expect(tag).to be_a(Gitlab::Git::Tag) + end end - it 'passes commit SHA to pre-receive and update hooks,\ - and tag SHA to post-receive hook' do + context 'with an invalid target' do + it 'returns false' do + expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false + end + end + end + + context 'when Gitaly operation_user_add_tag feature is enabled' do + it_behaves_like 'adding tag' + end + + context 'when Gitaly operation_user_add_tag feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'adding tag' + + 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', project) update_hook = Gitlab::Git::Hook.new('update', project) post_receive_hook = Gitlab::Git::Hook.new('post-receive', project) @@ -1660,12 +1686,6 @@ describe Repository, models: true do .with(anything, anything, tag_sha, anything) end end - - context 'with an invalid target' do - it 'returns false' do - expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false - end - end end describe '#rm_branch' do @@ -1680,12 +1700,22 @@ describe Repository, models: true do end describe '#rm_tag' do - it 'removes a tag' do - expect(repository).to receive(:before_remove_tag) + shared_examples 'removing tag' do + it 'removes a tag' do + expect(repository).to receive(:before_remove_tag) + + repository.rm_tag(build_stubbed(:user), 'v1.1.0') + + expect(repository.find_tag('v1.1.0')).to be_nil + end + end - repository.rm_tag(create(:user), 'v1.1.0') + context 'when Gitaly operation_user_delete_tag feature is enabled' do + it_behaves_like 'removing tag' + end - expect(repository.find_tag('v1.1.0')).to be_nil + context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'removing tag' end end diff --git a/spec/models/user_custom_attribute_spec.rb b/spec/models/user_custom_attribute_spec.rb new file mode 100644 index 00000000000..37fc3cb64f0 --- /dev/null +++ b/spec/models/user_custom_attribute_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe UserCustomAttribute do + describe 'assocations' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + subject { build :user_custom_attribute } + + it { is_expected.to validate_presence_of(:user_id) } + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_presence_of(:value) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:user_id) } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c1affa812aa..62890dd5002 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -39,6 +39,7 @@ describe User do it { is_expected.to have_many(:chat_names).dependent(:destroy) } it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') } + it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') } describe "#abuse_report" do let(:current_user) { create(:user) } diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index a6bf70c1e09..5b8cf2e6ab5 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -51,4 +51,41 @@ describe GlobalPolicy do end end end + + describe "create fork" do + context "when user has not exceeded project limit" do + it { is_expected.to be_allowed(:create_fork) } + end + + context "when user has exceeded project limit" do + let(:current_user) { create(:user, projects_limit: 0) } + + it { is_expected.not_to be_allowed(:create_fork) } + end + + context "when user is a master in a group" do + let(:group) { create(:group) } + let(:current_user) { create(:user, projects_limit: 0) } + + before do + group.add_master(current_user) + end + + it { is_expected.to be_allowed(:create_fork) } + end + end + + describe 'custom attributes' do + context 'regular user' do + it { is_expected.not_to be_allowed(:read_custom_attribute) } + it { is_expected.not_to be_allowed(:update_custom_attribute) } + end + + context 'admin' do + let(:current_user) { create(:user, :admin) } + + it { is_expected.to be_allowed(:read_custom_attribute) } + it { is_expected.to be_allowed(:update_custom_attribute) } + end + end end diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb new file mode 100644 index 00000000000..e52ff02e5f0 --- /dev/null +++ b/spec/policies/namespace_policy_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe NamespacePolicy do + let(:current_user) { create(:user) } + let(:namespace) { current_user.namespace } + + subject { described_class.new(current_user, namespace) } + + context "create projects" do + context "user namespace" do + it { is_expected.to be_allowed(:create_projects) } + end + + context "user who has exceeded project limit" do + let(:current_user) { create(:user, projects_limit: 0) } + + it { is_expected.not_to be_allowed(:create_projects) } + end + end +end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index c0cbdeed03d..f2593a1a75c 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe ProjectPolicy do - let(:guest) { create(:user) } - let(:reporter) { create(:user) } - let(:dev) { create(:user) } - let(:master) { create(:user) } - let(:owner) { create(:user) } - let(:admin) { create(:admin) } + set(:guest) { create(:user) } + set(:reporter) { create(:user) } + set(:developer) { create(:user) } + set(:master) { create(:user) } + set(:owner) { create(:user) } + set(:admin) { create(:admin) } let(:project) { create(:project, :public, namespace: owner.namespace) } - let(:guest_permissions) do + let(:base_guest_permissions) do %i[ read_project read_board read_list read_wiki read_issue read_label read_milestone read_project_snippet read_project_member @@ -18,7 +18,7 @@ describe ProjectPolicy do ] end - let(:reporter_permissions) do + let(:base_reporter_permissions) do %i[ download_code fork_project create_project_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build @@ -41,7 +41,7 @@ describe ProjectPolicy do ] end - let(:master_permissions) do + let(:base_master_permissions) do %i[ delete_protected_branch update_project_snippet update_environment update_deployment admin_project_snippet @@ -66,11 +66,20 @@ describe ProjectPolicy do ] end + # Used in EE specs + let(:additional_guest_permissions) { [] } + let(:additional_reporter_permissions) { [] } + let(:additional_master_permissions) { [] } + + let(:guest_permissions) { base_guest_permissions + additional_guest_permissions } + let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions } + let(:master_permissions) { base_master_permissions + additional_master_permissions } + before do - project.team << [guest, :guest] - project.team << [master, :master] - project.team << [dev, :developer] - project.team << [reporter, :reporter] + project.add_guest(guest) + project.add_master(master) + project.add_developer(developer) + project.add_reporter(reporter) end def expect_allowed(*permissions) @@ -127,38 +136,41 @@ describe ProjectPolicy do end end - context 'when a project has pending invites, and the current user is anonymous' do - let(:group) { create(:group, :public) } - let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] } - let(:anonymous_permissions) { guest_permissions - user_permissions } + shared_examples 'project policies as anonymous' do + context 'abilities for public projects' do + context 'when a project has pending invites' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } + let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] } + let(:anonymous_permissions) { guest_permissions - user_permissions } - subject { described_class.new(nil, project) } + subject { described_class.new(nil, project) } - before do - create(:group_member, :invited, group: group) - end + before do + create(:group_member, :invited, group: group) + end - it 'does not grant owner access' do - expect_allowed(*anonymous_permissions) - expect_disallowed(*user_permissions) + it 'does not grant owner access' do + expect_allowed(*anonymous_permissions) + expect_disallowed(*user_permissions) + end + end end - end - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } - subject { described_class.new(current_user, project) } - - context 'with no user' do - let(:current_user) { nil } + subject { described_class.new(nil, project) } it { is_expected.to be_banned } end + end - context 'guests' do - let(:current_user) { guest } + shared_examples 'project policies as guest' do + subject { described_class.new(guest, project) } + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } let(:reporter_public_build_permissions) do reporter_permissions - [:read_build, :read_pipeline] end @@ -179,7 +191,7 @@ describe ProjectPolicy do end end - context 'public builds disabled' do + context 'when public builds disabled' do before do project.update(public_builds: false) end @@ -192,8 +204,7 @@ describe ProjectPolicy do context 'when builds are disabled' do before do - project.project_feature.update( - builds_access_level: ProjectFeature::DISABLED) + project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) end it do @@ -202,9 +213,13 @@ describe ProjectPolicy do end end end + end + + shared_examples 'project policies as reporter' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } - context 'reporter' do - let(:current_user) { reporter } + subject { described_class.new(reporter, project) } it do expect_allowed(*guest_permissions) @@ -216,9 +231,13 @@ describe ProjectPolicy do expect_disallowed(*owner_permissions) end end + end - context 'developer' do - let(:current_user) { dev } + shared_examples 'project policies as developer' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(developer, project) } it do expect_allowed(*guest_permissions) @@ -229,9 +248,13 @@ describe ProjectPolicy do expect_disallowed(*owner_permissions) end end + end + + shared_examples 'project policies as master' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } - context 'master' do - let(:current_user) { master } + subject { described_class.new(master, project) } it do expect_allowed(*guest_permissions) @@ -242,9 +265,13 @@ describe ProjectPolicy do expect_disallowed(*owner_permissions) end end + end + + shared_examples 'project policies as owner' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } - context 'owner' do - let(:current_user) { owner } + subject { described_class.new(owner, project) } it do expect_allowed(*guest_permissions) @@ -255,9 +282,13 @@ describe ProjectPolicy do expect_allowed(*owner_permissions) end end + end - context 'admin' do - let(:current_user) { admin } + shared_examples 'project policies as admin' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(admin, project) } it do expect_allowed(*guest_permissions) @@ -269,4 +300,12 @@ describe ProjectPolicy do end end end + + it_behaves_like 'project policies as anonymous' + it_behaves_like 'project policies as guest' + it_behaves_like 'project policies as reporter' + it_behaves_like 'project policies as developer' + it_behaves_like 'project policies as master' + it_behaves_like 'project policies as owner' + it_behaves_like 'project policies as admin' end diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 6bd17697c33..50d0f72f6bc 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' describe API::AccessRequests do - let(:master) { create(:user) } - let(:developer) { create(:user) } - let(:access_requester) { create(:user) } - let(:stranger) { create(:user) } + set(:master) { create(:user) } + set(:developer) { create(:user) } + set(:access_requester) { create(:user) } + set(:stranger) { create(:user) } - let(:project) do + set(:project) do create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| project.team << [developer, :developer] project.team << [master, :master] @@ -14,7 +14,7 @@ describe API::AccessRequests do end end - let(:group) do + set(:group) do create(:group, :public, :access_requestable) do |group| group.add_developer(developer) group.add_owner(master) diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 593068b8cd7..7a0765c1fae 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' describe API::AwardEmoji do - let(:user) { create(:user) } - let!(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } - let!(:note) { create(:note, project: project, noteable: issue) } + set(:user) { create(:user) } + set(:project) { create(:project) } + set(:issue) { create(:issue, project: project) } + set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } + set(:note) { create(:note, project: project, noteable: issue) } before do project.team << [user, :master] diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index f698d5dddb3..fcfa4ddfbfe 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -1,34 +1,34 @@ require 'spec_helper' describe API::Boards do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:non_member) { create(:user) } - let(:guest) { create(:user) } - let(:admin) { create(:user, :admin) } - let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } - - let!(:dev_label) do + set(:user) { create(:user) } + set(:user2) { create(:user) } + set(:non_member) { create(:user) } + set(:guest) { create(:user) } + set(:admin) { create(:user, :admin) } + set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } + + set(:dev_label) do create(:label, title: 'Development', color: '#FFAABB', project: project) end - let!(:test_label) do + set(:test_label) do create(:label, title: 'Testing', color: '#FFAACC', project: project) end - let!(:ux_label) do + set(:ux_label) do create(:label, title: 'UX', color: '#FF0000', project: project) end - let!(:dev_list) do + set(:dev_list) do create(:list, label: dev_label, position: 1) end - let!(:test_list) do + set(:test_list) do create(:list, label: test_label, position: 2) end - let!(:board) do + set(:board) do create(:board, project: project, lists: [dev_list, test_list]) end @@ -187,8 +187,11 @@ describe API::Boards do end context "when the user is project owner" do - let(:owner) { create(:user) } - let(:project) { create(:project, namespace: owner.namespace) } + set(:owner) { create(:user) } + + before do + project.update(namespace: owner.namespace) + end it "deletes the list if an admin requests it" do delete api("#{base_url}/#{dev_list.id}", owner) diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index cc794fad3a7..16b12446ed4 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe API::Branches do - let(:user) { create(:user) } - let(:guest) { create(:user).tap { |u| project.add_guest(u) } } + set(:user) { create(:user) } let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + let(:guest) { create(:user).tap { |u| project.add_guest(u) } } let(:branch_name) { 'feature' } let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } @@ -40,7 +40,9 @@ describe API::Branches do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end it_behaves_like 'repository branches' end @@ -118,7 +120,9 @@ describe API::Branches do end context 'when unauthenticated', 'and project is public' do - let(:project) { create(:project, :public, :repository) } + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end it_behaves_like 'repository branch' end diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index b043a333d33..eacc575d97f 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe API::BroadcastMessages do - let(:user) { create(:user) } - let(:admin) { create(:admin) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } + set(:message) { create(:broadcast_message) } describe 'GET /broadcast_messages' do it 'returns a 401 for anonymous users' do @@ -31,8 +32,6 @@ describe API::BroadcastMessages do end describe 'GET /broadcast_messages/:id' do - let!(:message) { create(:broadcast_message) } - it 'returns a 401 for anonymous users' do get api("/broadcast_messages/#{message.id}") @@ -103,8 +102,6 @@ describe API::BroadcastMessages do end describe 'PUT /broadcast_messages/:id' do - let!(:message) { create(:broadcast_message) } - it 'returns a 401 for anonymous users' do put api("/broadcast_messages/#{message.id}"), attributes_for(:broadcast_message) @@ -155,8 +152,6 @@ describe API::BroadcastMessages do end describe 'DELETE /broadcast_messages/:id' do - let!(:message) { create(:broadcast_message) } - it 'returns a 401 for anonymous users' do delete api("/broadcast_messages/#{message.id}"), attributes_for(:broadcast_message) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index f663719d28c..94462b4572d 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -491,6 +491,7 @@ describe API::Commits do expect(json_response['stats']['deletions']).to eq(commit.stats.deletions) expect(json_response['stats']['total']).to eq(commit.stats.total) expect(json_response['status']).to be_nil + expect(json_response['last_pipeline']).to be_nil end context 'when ref does not exist' do @@ -573,6 +574,10 @@ describe API::Commits do expect(response).to have_http_status(200) expect(response).to match_response_schema('public_api/v4/commit/detail') expect(json_response['status']).to eq('created') + expect(json_response['last_pipeline']['id']).to eq(pipeline.id) + expect(json_response['last_pipeline']['ref']).to eq(pipeline.ref) + expect(json_response['last_pipeline']['sha']).to eq(pipeline.sha) + expect(json_response['last_pipeline']['status']).to eq(pipeline.status) end context 'when pipeline succeeds' do diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 2361809e0e1..f8cd529a06c 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -20,6 +20,7 @@ describe API::Environments do path path_with_namespace star_count forks_count created_at last_activity_at + avatar_url ) get api("/projects/#{project.id}/environments", user) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 42f0079e173..1671a046fdf 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -159,11 +159,14 @@ describe API::Groups do context 'when using owned in the request' do it 'returns an array of groups the user owns' do + group1.add_master(user2) + get api('/groups', user2), owned: true expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array + expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(group2.name) end end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index d4006fe71a2..98c49d3364c 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -159,18 +159,25 @@ describe API::Helpers do end describe "when authenticating using a user's private token" do - it "returns nil for an invalid token" do + it "returns a 401 response for an invalid token" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } - expect(current_user).to be_nil + expect { current_user }.to raise_error /401/ end - it "returns nil for a user without access" do + it "returns a 401 response for a user without access" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect(current_user).to be_nil + expect { current_user }.to raise_error /401/ + end + + it 'returns a 401 response for a user who is blocked' do + user.block! + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token + + expect { current_user }.to raise_error /401/ end it "leaves user as is when sudo not specified" do @@ -193,24 +200,31 @@ describe API::Helpers do allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } end - it "returns nil for an invalid token" do + it "returns a 401 response for an invalid token" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' - expect(current_user).to be_nil + expect { current_user }.to raise_error /401/ end - it "returns nil for a user without access" do + it "returns a 401 response for a user without access" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect(current_user).to be_nil + expect { current_user }.to raise_error /401/ + end + + it 'returns a 401 response for a user who is blocked' do + user.block! + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + + expect { current_user }.to raise_error /401/ end - it "returns nil for a token without the appropriate scope" do + it "returns a 401 response for a token without the appropriate scope" do personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect(current_user).to be_nil + expect { current_user }.to raise_error /401/ end it "leaves user as is when sudo not specified" do @@ -226,14 +240,14 @@ describe API::Helpers do personal_access_token.revoke! env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect(current_user).to be_nil + expect { current_user }.to raise_error /401/ end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect(current_user).to be_nil + expect { current_user }.to raise_error /401/ end end @@ -351,6 +365,18 @@ describe API::Helpers do end end end + + context 'when user is blocked' do + before do + user.block! + end + + it 'changes current_user to sudo' do + set_env(admin, user.id) + + expect(current_user).to eq(user) + end + end end context 'with regular user' do @@ -490,11 +516,10 @@ describe API::Helpers do context 'current_user is nil' do before do expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) - allow_any_instance_of(self.class).to receive(:initial_current_user).and_return(nil) end it 'returns a 401 response' do - expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' + expect { authenticate! }.to raise_error /401/ end end @@ -502,35 +527,12 @@ describe API::Helpers do let(:user) { build(:user) } before do - expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user) - expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user) + expect_any_instance_of(self.class).to receive(:current_user).and_return(user) end it 'does not raise an error' do expect { authenticate! }.not_to raise_error end end - - context 'current_user is blocked' do - let(:user) { build(:user, :blocked) } - - before do - expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user) - end - - it 'raises an error' do - expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user) - - expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' - end - - it "doesn't raise an error if an admin user is impersonating a blocked user (via sudo)" do - admin_user = build(:user, :admin) - - expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(admin_user) - - expect { authenticate! }.not_to raise_error - end - end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 92e7d797cbd..18f6f7df1fa 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -193,6 +193,7 @@ describe API::Projects do path path_with_namespace star_count forks_count created_at last_activity_at + avatar_url ) get api('/projects?simple=true', user) @@ -1181,6 +1182,59 @@ describe API::Projects do end end end + + describe 'GET /projects/:id/forks' do + let(:private_fork) { create(:project, :private, :empty_repo) } + let(:member) { create(:user) } + let(:non_member) { create(:user) } + + before do + private_fork.add_developer(member) + end + + context 'for a forked project' do + before do + post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin) + private_fork.reload + expect(private_fork.forked_from_project).not_to be_nil + expect(private_fork.forked?).to be_truthy + project_fork_source.reload + expect(project_fork_source.forks.length).to eq(1) + expect(project_fork_source.forks).to include(private_fork) + end + + context 'for a user that can access the forks' do + it 'returns the forks' do + get api("/projects/#{project_fork_source.id}/forks", member) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(1) + expect(json_response[0]['name']).to eq(private_fork.name) + end + end + + context 'for a user that cannot access the forks' do + it 'returns an empty array' do + get api("/projects/#{project_fork_source.id}/forks", non_member) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(0) + end + end + end + + context 'for a non-forked project' do + it 'returns an empty array' do + get api("/projects/#{project_fork_source.id}/forks") + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(0) + end + end + end end describe "POST /projects/:id/share" do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5b306ec6cbf..69c8aa4482a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -125,6 +125,15 @@ describe API::Users do end context "when admin" do + context 'when sudo is defined' do + it 'does not return 500' do + admin_personal_access_token = create(:personal_access_token, user: admin).token + get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin) + + expect(response).to have_http_status(:success) + end + end + it "returns an array of users" do get api("/users", admin) @@ -1896,4 +1905,8 @@ describe API::Users do expect(impersonation_token.reload.revoked).to be_truthy end end + + include_examples 'custom attributes endpoints', 'users' do + let(:attributable) { user } + end end diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb index 681e8e04295..36d793f505d 100644 --- a/spec/requests/api/v3/award_emoji_spec.rb +++ b/spec/requests/api/v3/award_emoji_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' describe API::V3::AwardEmoji do - let(:user) { create(:user) } - let!(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } - let!(:note) { create(:note, project: project, noteable: issue) } + set(:user) { create(:user) } + set(:project) { create(:project) } + set(:issue) { create(:issue, project: project) } + set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } + set(:note) { create(:note, project: project, noteable: issue) } before { project.team << [user, :master] } diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb index b86aab2ec70..ea2627142bf 100644 --- a/spec/requests/api/v3/boards_spec.rb +++ b/spec/requests/api/v3/boards_spec.rb @@ -1,28 +1,28 @@ require 'spec_helper' describe API::V3::Boards do - let(:user) { create(:user) } - let(:guest) { create(:user) } - let(:non_member) { create(:user) } - let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } + set(:user) { create(:user) } + set(:guest) { create(:user) } + set(:non_member) { create(:user) } + set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } - let!(:dev_label) do + set(:dev_label) do create(:label, title: 'Development', color: '#FFAABB', project: project) end - let!(:test_label) do + set(:test_label) do create(:label, title: 'Testing', color: '#FFAACC', project: project) end - let!(:dev_list) do + set(:dev_list) do create(:list, label: dev_label, position: 1) end - let!(:test_list) do + set(:test_list) do create(:list, label: test_label, position: 2) end - let!(:board) do + set(:board) do create(:board, project: project, lists: [dev_list, test_list]) end @@ -98,8 +98,11 @@ describe API::V3::Boards do end context "when the user is project owner" do - let(:owner) { create(:user) } - let(:project) { create(:project, namespace: owner.namespace) } + set(:owner) { create(:user) } + + before do + project.update(namespace: owner.namespace) + end it "deletes the list if an admin requests it" do delete v3_api("#{base_url}/#{dev_list.id}", owner) diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb index c88f7788697..9cd11a67712 100644 --- a/spec/requests/api/v3/branches_spec.rb +++ b/spec/requests/api/v3/branches_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' require 'mime/types' describe API::V3::Branches do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + set(:user) { create(:user) } + set(:user2) { create(:user) } + set(:project) { create(:project, :repository, creator: user) } + set(:master) { create(:project_member, :master, user: user, project: project) } + set(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb index 948cd78c177..d04b1c72004 100644 --- a/spec/requests/api/v3/broadcast_messages_spec.rb +++ b/spec/requests/api/v3/broadcast_messages_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe API::V3::BroadcastMessages do - let(:user) { create(:user) } - let(:admin) { create(:admin) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } describe 'DELETE /broadcast_messages/:id' do - let!(:message) { create(:broadcast_message) } + set(:message) { create(:broadcast_message) } it 'returns a 401 for anonymous users' do delete v3_api("/broadcast_messages/#{message.id}"), diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb index dc95599546c..0a2ff1058e3 100644 --- a/spec/requests/api/v3/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' describe API::V3::Builds do - let(:user) { create(:user) } + set(:user) { create(:user) } let(:api_user) { user } - let!(:project) { create(:project, :repository, creator: user, public_builds: false) } - let!(:developer) { create(:project_member, :developer, user: user, project: project) } - let(:reporter) { create(:project_member, :reporter, project: project) } - let(:guest) { create(:project_member, :guest, project: project) } - let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } + set(:project) { create(:project, :repository, creator: user, public_builds: false) } + set(:developer) { create(:project_member, :developer, user: user, project: project) } + set(:reporter) { create(:project_member, :reporter, project: project) } + set(:guest) { create(:project_member, :guest, project: project) } + set(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } let!(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 9a0e6647ebf..86768d7397a 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' describe API::V3::Issues, :mailer do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:non_member) { create(:user) } - let(:guest) { create(:user) } - let(:author) { create(:author) } - let(:assignee) { create(:assignee) } - let(:admin) { create(:user, :admin) } + set(:user) { create(:user) } + set(:user2) { create(:user) } + set(:non_member) { create(:user) } + set(:guest) { create(:user) } + set(:author) { create(:author) } + set(:assignee) { create(:assignee) } + set(:admin) { create(:user, :admin) } let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } let!(:closed_issue) do create :closed_issue, @@ -822,7 +822,8 @@ describe API::V3::Issues, :mailer do end context 'resolving issues in a merge request' do - let(:discussion) { create(:diff_note_on_merge_request).to_discussion } + set(:diff_note_on_merge_request) { create(:diff_note_on_merge_request) } + let(:discussion) { diff_note_on_merge_request.to_discussion } let(:merge_request) { discussion.noteable } let(:project) { merge_request.source_project } before do @@ -1169,7 +1170,7 @@ describe API::V3::Issues, :mailer do end context "when the user is project owner" do - let(:owner) { create(:user) } + set(:owner) { create(:user) } let(:project) { create(:project, namespace: owner.namespace) } it "deletes the issue if an admin requests it" do diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index cae2c3118da..e5282c3311f 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -89,6 +89,7 @@ describe API::V3::Projects do path path_with_namespace star_count forks_count created_at last_activity_at + avatar_url ) get v3_api('/projects?simple=true', user) diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 2de8daba6b5..3baf9b1edab 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -103,9 +103,15 @@ describe PipelineSerializer do let(:project) { create(:project) } before do - Ci::Pipeline::AVAILABLE_STATUSES.each do |status| - create_pipeline(status) + # Since RequestStore.active? is true we have to allow the + # gitaly calls in this block + # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/37772 + Gitlab::GitalyClient.allow_n_plus_1_calls do + Ci::Pipeline::AVAILABLE_STATUSES.each do |status| + create_pipeline(status) + end end + Gitlab::GitalyClient.reset_counts end shared_examples 'no N+1 queries' do diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4c2ff08039c..eb6e683cc23 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -133,6 +133,26 @@ describe Ci::CreatePipelineService do expect(merge_request.reload.head_pipeline).to eq head_pipeline end end + + context 'when pipeline has been skipped' do + before do + allow_any_instance_of(Ci::Pipeline) + .to receive(:git_commit_message) + .and_return('some commit [ci skip]') + end + + it 'updates merge request head pipeline' do + merge_request = create(:merge_request, source_branch: 'master', + target_branch: 'feature', + source_project: project) + + head_pipeline = execute_service + + expect(head_pipeline).to be_skipped + expect(head_pipeline).to be_persisted + expect(merge_request.reload.head_pipeline).to eq head_pipeline + end + end end context 'auto-cancel enabled' do @@ -481,104 +501,4 @@ describe Ci::CreatePipelineService do end end end - - describe '#allowed_to_create?' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:ref) { 'master' } - - subject do - described_class.new(project, user, ref: ref) - .send(:allowed_to_create?) - end - - context 'when user is a developer' do - before do - project.add_developer(user) - end - - it { is_expected.to be_truthy } - - context 'when the branch is protected' do - let!(:protected_branch) do - create(:protected_branch, project: project, name: ref) - end - - it { is_expected.to be_falsey } - - context 'when developers are allowed to merge' do - let!(:protected_branch) do - create(:protected_branch, - :developers_can_merge, - project: project, - name: ref) - end - - it { is_expected.to be_truthy } - end - end - - context 'when the tag is protected' do - let(:ref) { 'v1.0.0' } - - let!(:protected_tag) do - create(:protected_tag, project: project, name: ref) - end - - it { is_expected.to be_falsey } - - context 'when developers are allowed to create the tag' do - let!(:protected_tag) do - create(:protected_tag, - :developers_can_create, - project: project, - name: ref) - end - - it { is_expected.to be_truthy } - end - end - end - - context 'when user is a master' do - before do - project.add_master(user) - end - - it { is_expected.to be_truthy } - - context 'when the branch is protected' do - let!(:protected_branch) do - create(:protected_branch, project: project, name: ref) - end - - it { is_expected.to be_truthy } - end - - context 'when the tag is protected' do - let(:ref) { 'v1.0.0' } - - let!(:protected_tag) do - create(:protected_tag, project: project, name: ref) - end - - it { is_expected.to be_truthy } - - context 'when no one can create the tag' do - let!(:protected_tag) do - create(:protected_tag, - :no_one_can_create, - project: project, - name: ref) - end - - it { is_expected.to be_falsey } - end - end - end - - context 'when owner cannot create pipeline' do - it { is_expected.to be_falsey } - end - end end diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb index 641d5538de8..75812c17309 100644 --- a/spec/services/emails/create_service_spec.rb +++ b/spec/services/emails/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Emails::CreateService do let(:user) { create(:user) } - let(:opts) { { email: 'new@email.com' } } + let(:opts) { { email: 'new@email.com', user: user } } subject(:service) { described_class.new(user, opts) } diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb index 1f4294dd905..7726fc0ef81 100644 --- a/spec/services/emails/destroy_service_spec.rb +++ b/spec/services/emails/destroy_service_spec.rb @@ -4,7 +4,7 @@ describe Emails::DestroyService do let!(:user) { create(:user) } let!(:email) { create(:email, user: user) } - subject(:service) { described_class.new(user, email: email.email) } + subject(:service) { described_class.new(user, user: user, email: email.email) } describe '#execute' do it 'removes an email' do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 171f70c32a8..5c27e8fd561 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -42,7 +42,7 @@ describe Issues::CloseService do service.execute(issue) end - it 'refreshes the number of open issues' do + it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do expect { service.execute(issue) } .to change { project.open_issues_count }.from(1).to(0) end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index cc3d648c340..d86da244520 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -35,7 +35,7 @@ describe Issues::CreateService do expect(issue.due_date).to eq Date.tomorrow end - it 'refreshes the number of open issues' do + it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do expect { issue }.to change { project.open_issues_count }.from(0).to(1) end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 15a50b85f19..a8a8aeed1bd 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -64,6 +64,13 @@ describe Issues::UpdateService, :mailer do expect(issue.due_date).to eq Date.tomorrow end + it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do + issue # make sure the issue is created first so our counts are correct. + + expect { update_issue(confidential: true) } + .to change { project.open_issues_count }.from(1).to(0) + end + it 'updates open issue counter for assignees when issue is reassigned' do update_issue(assignee_ids: [user2.id]) diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb new file mode 100644 index 00000000000..bb0fb6acf39 --- /dev/null +++ b/spec/services/keys/last_used_service_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Keys::LastUsedService do + describe '#execute', :clean_gitlab_redis_shared_state do + it 'updates the key when it has not been used recently' do + key = create(:key, last_used_at: 1.year.ago) + time = Time.zone.now + + Timecop.freeze(time) { described_class.new(key).execute } + + expect(key.last_used_at).to eq(time) + end + + it 'does not update the key when it has been used recently' do + time = 1.minute.ago + key = create(:key, last_used_at: time) + + described_class.new(key).execute + + expect(key.last_used_at).to eq(time) + end + + it 'does not update the updated_at field' do + # Since a lot of these updates could happen in parallel for different keys + # we want these updates to be as lightweight as possible, hence we want to + # make sure we _only_ update last_used_at and not always updated_at. + key = create(:key, last_used_at: 1.year.ago) + + expect { described_class.new(key).execute }.not_to change { key.updated_at } + end + end + + describe '#update?', :clean_gitlab_redis_shared_state do + it 'returns true when no last used timestamp is present' do + key = build(:key, last_used_at: nil) + service = described_class.new(key) + + expect(service.update?).to eq(true) + end + + it 'returns true when the key needs to be updated' do + key = build(:key, last_used_at: 1.year.ago) + service = described_class.new(key) + + expect(service.update?).to eq(true) + end + + it 'returns false when a lease has already been obtained' do + key = build(:key, last_used_at: 1.year.ago) + service = described_class.new(key) + + expect(service.update?).to eq(true) + expect(service.update?).to eq(false) + end + + it 'returns false when the key does not yet need to be updated' do + key = build(:key, last_used_at: 1.minute.ago) + service = described_class.new(key) + + expect(service.update?).to eq(false) + end + end +end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 7e65369762c..b3886987316 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -52,7 +52,7 @@ describe MergeRequests::CloseService do end end - it 'refreshes the number of open merge requests for a valid MR' do + it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do service = described_class.new(project, user, {}) expect { service.execute(merge_request) } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index d6409c0d625..a047f891ab2 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -37,7 +37,7 @@ describe MergeRequests::CreateService do expect(service).to have_received(:execute_hooks).with(merge_request) end - it 'refreshes the number of open merge requests' do + it 'refreshes the number of open merge requests', :use_clean_rails_memory_store_caching do expect { service.execute } .to change { project.open_merge_requests_count }.from(0).to(1) end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index b60136064b7..80213d093f1 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -13,20 +13,21 @@ describe MergeRequests::MergeService do describe '#execute' do context 'MergeRequest#merge_jid' do + let(:service) do + described_class.new(project, user, commit_message: 'Awesome message') + end + before do merge_request.update_column(:merge_jid, 'hash-123') end it 'is cleaned when no error is raised' do - service = described_class.new(project, user, commit_message: 'Awesome message') - service.execute(merge_request) expect(merge_request.reload.merge_jid).to be_nil end it 'is cleaned when expected error is raised' do - service = described_class.new(project, user, commit_message: 'Awesome message') allow(service).to receive(:commit).and_raise(described_class::MergeError) service.execute(merge_request) @@ -34,6 +35,22 @@ describe MergeRequests::MergeService do expect(merge_request.reload.merge_jid).to be_nil end + it 'is cleaned when merge request is not mergeable' do + allow(merge_request).to receive(:mergeable?).and_return(false) + + service.execute(merge_request) + + expect(merge_request.reload.merge_jid).to be_nil + end + + it 'is cleaned when no source is found' do + allow(merge_request).to receive(:diff_head_sha).and_return(nil) + + service.execute(merge_request) + + expect(merge_request.reload.merge_jid).to be_nil + end + it 'is not cleaned when unexpected error is raised' do service = described_class.new(project, user, commit_message: 'Awesome message') allow(service).to receive(:commit).and_raise(StandardError) diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index a37cdab8928..d2bd05d921f 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -11,5 +11,16 @@ describe MergeRequests::PostMergeService do describe '#execute' do it_behaves_like 'cache counters invalidator' + + it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do + # Cache the counter before the MR changed state. + project.open_merge_requests_count + merge_request.update!(state: 'merged') + + service = described_class.new(project, user, {}) + + expect { service.execute(merge_request) } + .to change { project.open_merge_requests_count }.from(1).to(0) + end end end diff --git a/spec/services/projects/count_service_spec.rb b/spec/services/projects/count_service_spec.rb index 79b01e7620e..cc496501bad 100644 --- a/spec/services/projects/count_service_spec.rb +++ b/spec/services/projects/count_service_spec.rb @@ -66,8 +66,8 @@ describe Projects::CountService do describe '#cache_key' do it 'returns the cache key as an Array' do - allow(service).to receive(:cache_key_name).and_return('count_service') - expect(service.cache_key).to eq(['projects', 1, 'count_service']) + allow(service).to receive(:cache_key_name).and_return('foo') + expect(service.cache_key).to eq(['projects', 'count_service', described_class::VERSION, 1, 'foo']) end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 5da634e2fb1..c2ec805ea99 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -156,10 +156,11 @@ describe Projects::CreateService, '#execute' do } end - let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] } + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } before do - gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing") + gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") end after do diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index a6e0364d44c..fa9d6969830 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -76,10 +76,11 @@ describe Projects::ForkService do end context 'repository already exists' do - let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] } + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } before do - gitlab_shell.add_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}") + gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}") end after do diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb new file mode 100644 index 00000000000..1b61207b550 --- /dev/null +++ b/spec/services/projects/hashed_storage_migration_service_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Projects::HashedStorageMigrationService do + let(:gitlab_shell) { Gitlab::Shell.new } + let(:project) { create(:project, :empty_repo, :wiki_repo) } + let(:service) { described_class.new(project) } + let(:legacy_storage) { Storage::LegacyProject.new(project) } + let(:hashed_storage) { Storage::HashedProject.new(project) } + + describe '#execute' do + before do + allow(service).to receive(:gitlab_shell) { gitlab_shell } + end + + context 'when succeeds' do + it 'renames project and wiki repositories' do + service.execute + + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy + end + + it 'updates project to be hashed and not readonly' do + service.execute + + expect(project.hashed_storage?).to be_truthy + expect(project.repository_read_only).to be_falsey + end + + it 'move operation is called for both repositories' do + expect_move_repository(project.disk_path, hashed_storage.disk_path) + expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") + + service.execute + end + end + + context 'when one move fails' do + it 'rollsback repositories to original name' do + from_name = project.disk_path + to_name = hashed_storage.disk_path + allow(service).to receive(:move_repository).and_call_original + allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only + + expect(service).to receive(:rollback_folder_move).and_call_original + + service.execute + + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey + end + + context 'when rollback fails' do + before do + from_name = legacy_storage.disk_path + to_name = hashed_storage.disk_path + + hashed_storage.ensure_storage_path_exists + gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) + end + + it 'does not try to move nil repository over hashed' do + expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") + + service.execute + end + end + end + + def expect_move_repository(from_name, to_name) + expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original + end + end +end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index a14ed526f68..2459f371a91 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -121,11 +121,14 @@ describe Projects::TransferService do end context 'namespace which contains orphan repository with same projects path name' do - let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] } + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } before do group.add_owner(user) - gitlab_shell.add_repository(repository_storage_path, "#{group.full_path}/#{project.path}") + unless gitlab_shell.add_repository(repository_storage, "#{group.full_path}/#{project.path}") + raise 'failed to add repository' + end @result = transfer_project(project, user, group) end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index c551083ac90..4873e967535 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -149,10 +149,11 @@ describe Projects::UpdateService, '#execute' do end context 'when renaming a project' do - let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] } + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } before do - gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing") + gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") end after do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index c2d6d7781b9..b1241cd8d0b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -232,7 +232,9 @@ describe SystemNoteService do context 'when milestone added' do it 'sets the note text' do - expect(subject.note).to eq "changed milestone to #{milestone.to_reference}" + reference = milestone.to_reference(format: :iid) + + expect(subject.note).to eq "changed milestone to #{reference}" end end diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb index 57013b54560..e7e9080b6b0 100644 --- a/spec/services/tags/create_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -28,7 +28,7 @@ describe Tags::CreateService do it 'returns an error' do expect(repository).to receive(:add_tag) .with(user, 'v1.1.0', 'master', 'Foo') - .and_raise(Rugged::TagError) + .and_raise(Gitlab::Git::Repository::TagExistsError) response = service.execute('v1.1.0', 'master', 'Foo') diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index 6ee35a33b2d..f8d4a47b212 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -31,13 +31,13 @@ describe Users::UpdateService do end def update_user(user, opts) - described_class.new(user, opts).execute + described_class.new(user, opts.merge(user: user)).execute end end describe '#execute!' do it 'updates the name' do - service = described_class.new(user, name: 'New Name') + service = described_class.new(user, user: user, name: 'New Name') expect(service).not_to receive(:notify_new_user) result = service.execute! @@ -55,7 +55,7 @@ describe Users::UpdateService do it 'fires system hooks when a new user is saved' do system_hook_service = spy(:system_hook_service) user = build(:user) - service = described_class.new(user, name: 'John Doe') + service = described_class.new(user, user: user, name: 'John Doe') expect(service).to receive(:notify_new_user).and_call_original expect(service).to receive(:system_hook_service).and_return(system_hook_service) @@ -65,7 +65,7 @@ describe Users::UpdateService do end def update_user(user, opts) - described_class.new(user, opts).execute! + described_class.new(user, opts.merge(user: user)).execute! end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 92735336572..dbf05b7f004 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -64,8 +64,16 @@ RSpec.configure do |config| config.infer_spec_type_from_file_location! - config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata| - metadata[:api] = true + config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| + location = metadata[:location] + + metadata[:api] = true if location =~ %r{/spec/requests/api/} + + # do not overwrite type if it's already set + next if metadata.key?(:type) + + match = location.match(%r{/spec/([^/]+)/}) + metadata[:type] = match[1].singularize.to_sym if match end config.raise_errors_for_deprecations! diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb index 5b6d9c1a4df..63f59b9654c 100644 --- a/spec/support/matchers/navigation_matcher.rb +++ b/spec/support/matchers/navigation_matcher.rb @@ -4,3 +4,9 @@ RSpec::Matchers.define :have_active_navigation do |expected| expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected) end end + +RSpec::Matchers.define :have_active_sub_navigation do |expected| + match do |page| + expect(page.find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(expected) + end +end diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb new file mode 100644 index 00000000000..c9302f7b750 --- /dev/null +++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb @@ -0,0 +1,103 @@ +shared_examples 'custom attributes endpoints' do |attributable_name| + let!(:custom_attribute1) { attributable.custom_attributes.create key: 'foo', value: 'foo' } + let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' } + + describe "GET /#{attributable_name} with custom attributes filter" do + let!(:other_attributable) { create attributable.class.name.underscore } + + context 'with an unauthorized user' do + it 'does not filter by custom attributes' do + get api("/#{attributable_name}", user), custom_attributes: { foo: 'foo', bar: 'bar' } + + expect(response).to have_http_status(200) + expect(json_response.size).to be 2 + end + end + + it 'filters by custom attributes' do + get api("/#{attributable_name}", admin), custom_attributes: { foo: 'foo', bar: 'bar' } + + expect(response).to have_http_status(200) + expect(json_response.size).to be 1 + expect(json_response.first['id']).to eq attributable.id + end + end + + describe "GET /#{attributable_name}/:id/custom_attributes" do + context 'with an unauthorized user' do + subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes", user) } + + it_behaves_like 'an unauthorized API user' + end + + it 'returns all custom attributes' do + get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin) + + expect(response).to have_http_status(200) + expect(json_response).to contain_exactly( + { 'key' => 'foo', 'value' => 'foo' }, + { 'key' => 'bar', 'value' => 'bar' } + ) + end + end + + describe "GET /#{attributable_name}/:id/custom_attributes/:key" do + context 'with an unauthorized user' do + subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) } + + it_behaves_like 'an unauthorized API user' + end + + it 'returns a single custom attribute' do + get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) + + expect(response).to have_http_status(200) + expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' }) + end + end + + describe "PUT /#{attributable_name}/:id/custom_attributes/:key" do + context 'with an unauthorized user' do + subject { put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user), value: 'new' } + + it_behaves_like 'an unauthorized API user' + end + + it 'creates a new custom attribute' do + expect do + put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), value: 'new' + end.to change { attributable.custom_attributes.count }.by(1) + + expect(response).to have_http_status(200) + expect(json_response).to eq({ 'key' => 'new', 'value' => 'new' }) + expect(attributable.custom_attributes.find_by(key: 'new').value).to eq 'new' + end + + it 'updates an existing custom attribute' do + expect do + put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), value: 'new' + end.not_to change { attributable.custom_attributes.count } + + expect(response).to have_http_status(200) + expect(json_response).to eq({ 'key' => 'foo', 'value' => 'new' }) + expect(custom_attribute1.reload.value).to eq 'new' + end + end + + describe "DELETE /#{attributable_name}/:id/custom_attributes/:key" do + context 'with an unauthorized user' do + subject { delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) } + + it_behaves_like 'an unauthorized API user' + end + + it 'deletes an existing custom attribute' do + expect do + delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) + end.to change { attributable.custom_attributes.count }.by(-1) + + expect(response).to have_http_status(204) + expect(attributable.custom_attributes.find_by(key: 'foo')).to be_nil + end + end +end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 45c10e78789..2dfb4d4a07f 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -42,7 +42,7 @@ module StubConfiguration # Default storage is always required messages['default'] ||= Gitlab.config.repositories.storages.default messages.each do |storage_name, storage_settings| - storage_settings['path'] ||= TestEnv.repos_path + storage_settings['path'] = TestEnv.repos_path unless storage_settings.key?('path') storage_settings['failure_count_threshold'] ||= 10 storage_settings['failure_wait_time'] ||= 30 storage_settings['failure_reset_time'] ||= 1800 diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 6e5b9700b54..b4e8b5ea67b 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -306,6 +306,9 @@ module TestEnv ensure_component_dir_name_is_correct!(component, install_dir) + # On CI, once installed, components never need update + return if File.exist?(install_dir) && ENV['CI'] + if component_needs_update?(install_dir, version) # Cleanup the component entirely to ensure we start fresh FileUtils.rm_rf(install_dir) diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb new file mode 100644 index 00000000000..f59792c3d36 --- /dev/null +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -0,0 +1,52 @@ +require 'rake_helper' + +describe 'gitlab:storage rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/storage' + + stub_warn_user_is_not_gitlab + end + + describe 'migrate_to_hashed rake task' do + context '0 legacy projects' do + it 'does nothing' do + expect(StorageMigratorWorker).not_to receive(:perform_async) + + run_rake_task('gitlab:storage:migrate_to_hashed') + end + end + + context '5 legacy projects' do + let(:projects) { create_list(:project, 5, storage_version: 0) } + + context 'in batches of 1' do + before do + stub_env('BATCH' => 1) + end + + it 'enqueues one StorageMigratorWorker per project' do + projects.each do |project| + expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id) + end + + run_rake_task('gitlab:storage:migrate_to_hashed') + end + end + + context 'in batches of 2' do + before do + stub_env('BATCH' => 2) + end + + it 'enqueues one StorageMigratorWorker per 2 projects' do + projects.map(&:id).sort.each_slice(2) do |first, last| + last ||= first + expect(StorageMigratorWorker).to receive(:perform_async).with(first, last) + end + + run_rake_task('gitlab:storage:migrate_to_hashed') + end + end + end + end +end diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index d34617be474..fae5ec35c47 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -75,4 +75,24 @@ describe Gitlab::TaskHelpers do subject.checkout_version(tag, clone_path) end end + + describe '#run_command' do + it 'runs command and return the output' do + expect(subject.run_command(%w(echo it works!))).to eq("it works!\n") + end + + it 'returns empty string when command doesnt exist' do + expect(subject.run_command(%w(nonexistentcommand with arguments))).to eq('') + end + end + + describe '#run_command!' do + it 'runs command and return the output' do + expect(subject.run_command!(%w(echo it works!))).to eq("it works!\n") + end + + it 'returns and exception when command exit with non zero code' do + expect { subject.run_command!(['bash', '-c', 'exit 1']) }.to raise_error Gitlab::TaskFailedError + end + end end diff --git a/spec/views/dashboard/projects/_nav.html.haml.rb b/spec/views/dashboard/projects/_nav.html.haml.rb new file mode 100644 index 00000000000..f6a8ca13040 --- /dev/null +++ b/spec/views/dashboard/projects/_nav.html.haml.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'dashboard/projects/_nav.html.haml' do + it 'highlights All tab by default' do + render + + expect(rendered).to have_css('li.active a', text: 'All') + end + + it 'highlights Personal tab personal param is present' do + controller.params[:personal] = true + + render + + expect(rendered).to have_css('li.active a', text: 'Personal') + end +end diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb new file mode 100644 index 00000000000..c757ccf02d3 --- /dev/null +++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe 'projects/pipelines_settings/_show' do + let(:project) { create(:project, :repository) } + + before do + assign :project, project + end + + context 'when kubernetes is not active' do + context 'when auto devops domain is not defined' do + it 'shows warning message' do + render + + expect(rendered).to have_css('.settings-message') + expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and the') + expect(rendered).to have_link('Kubernetes service') + end + end + + context 'when auto devops domain is defined' do + before do + project.build_auto_devops(domain: 'example.com') + end + + it 'shows warning message' do + render + + expect(rendered).to have_css('.settings-message') + expect(rendered).to have_text('Auto Review Apps and Auto Deploy need the') + expect(rendered).to have_link('Kubernetes service') + end + end + end + + context 'when kubernetes is active' do + before do + project.build_kubernetes_service(active: true) + end + + context 'when auto devops domain is not defined' do + it 'shows warning message' do + render + + expect(rendered).to have_css('.settings-message') + expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.') + end + end + + context 'when auto devops domain is defined' do + before do + project.build_auto_devops(domain: 'example.com') + end + + it 'does not show warning message' do + render + + expect(rendered).not_to have_css('.settings-message') + end + end + end +end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index f7b67b8efc6..ab656d619f4 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -32,7 +32,7 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id, :gc, lease_key, lease_uuid) end @@ -47,7 +47,6 @@ describe GitGarbageCollectWorker do expect(subject).not_to receive(:command) expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original - expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original subject.perform(project.id, :gc, lease_key, lease_uuid) @@ -78,7 +77,7 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id) end @@ -93,7 +92,6 @@ describe GitGarbageCollectWorker do expect(subject).not_to receive(:command) expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original - expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original subject.perform(project.id) diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index d3707a3cc11..05eecf5f0bb 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -70,12 +70,15 @@ describe PostReceive do context "creates a Ci::Pipeline for every change" do before do - allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do - OpenStruct.new(id: '123456') - end - allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true) - allow_any_instance_of(Repository).to receive(:ref_exists?).and_return(true) stub_ci_pipeline_to_return_yaml_file + + # TODO, don't stub private methods + # + allow_any_instance_of(Ci::CreatePipelineService) + .to receive(:commit).and_return(OpenStruct.new(id: '123456')) + + allow_any_instance_of(Repository) + .to receive(:branch_exists?).and_return(true) end it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb new file mode 100644 index 00000000000..f5226dee0ad --- /dev/null +++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe ProjectMigrateHashedStorageWorker do + describe '#perform' do + let(:project) { create(:project, :empty_repo) } + let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) } + + it 'skips when project no longer exists' do + nonexistent_id = 999999999999 + + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + subject.perform(nonexistent_id) + end + + it 'skips when project is pending delete' do + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + + subject.perform(pending_delete_project.id) + end + + it 'delegates removal to service class' do + service = double('service') + expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service) + expect(service).to receive(:execute) + + subject.perform(project.id) + end + end +end diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/storage_migrator_worker_spec.rb new file mode 100644 index 00000000000..8619ff2f7da --- /dev/null +++ b/spec/workers/storage_migrator_worker_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe StorageMigratorWorker do + subject(:worker) { described_class.new } + let(:projects) { create_list(:project, 2) } + + describe '#perform' do + let(:ids) { projects.map(&:id) } + + it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do + expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice + + worker.perform(ids.min, ids.max) + end + + it 'sets projects as read only' do + allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice + worker.perform(ids.min, ids.max) + + projects.each do |project| + expect(project.reload.repository_read_only?).to be_truthy + end + end + + it 'rescues and log exceptions' do + allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) + expect { worker.perform(ids.min, ids.max) }.not_to raise_error + end + end +end diff --git a/spec/workers/use_key_worker_spec.rb b/spec/workers/use_key_worker_spec.rb deleted file mode 100644 index e50c788b82a..00000000000 --- a/spec/workers/use_key_worker_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe UseKeyWorker do - describe "#perform" do - it "updates the key's last_used_at attribute to the current time when it exists" do - worker = described_class.new - key = create(:key) - current_time = Time.zone.now - - Timecop.freeze(current_time) do - expect { worker.perform(key.id) } - .to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time) - end - end - - it "returns false and skips the job when the key doesn't exist" do - worker = described_class.new - key = create(:key) - - expect(worker.perform(key.id + 1)).to eq false - end - end -end diff --git a/symbol/icons.svg b/symbol/icons.svg new file mode 100644 index 00000000000..433bd2aca0d --- /dev/null +++ b/symbol/icons.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 1792 1792" id="clock_o" xmlns="http://www.w3.org/2000/svg"><path d="M1024 544v448q0 14-9 23t-23 9H672q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224V544q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5T1281.5 1561 896 1664t-385.5-103T231 1281.5 128 896t103-385.5T510.5 231 896 128t385.5 103T1561 510.5 1664 896z"/></symbol><symbol viewBox="0 0 36 18" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M34 7h-7.2c-.9-4-4.5-7-8.8-7s-7.9 3-8.8 7H2C.9 7 0 7.9 0 9s.9 2 2 2h7.2c.9 4 4.5 7 8.8 7s7.9-3 8.8-7H34c1.1 0 2-.9 2-2s-.9-2-2-2m-16 7c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol></svg>
\ No newline at end of file diff --git a/symbol/sprite.symbol.html b/symbol/sprite.symbol.html new file mode 100644 index 00000000000..a2289014093 --- /dev/null +++ b/symbol/sprite.symbol.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8"/> + <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> + <script src="https://rawgit.com/jonathantneal/svg4everybody/master/dist/svg4everybody.js"></script> + <script>svg4everybody();</script> + <title>SVG <symbol> sprite preview | svg-sprite</title> + <style>@charset "UTF-8";body{padding:0;margin:0;color:#666;background:#fafafa;font-family:Arial,Helvetica,sans-serif;font-size:1em;line-height:1.4}header{display:block;padding:3em 3em 2em 3em;background-color:#fff}header p{margin:2em 0 0 0}section{border-top:1px solid #eee;padding:2em 3em 0 3em}section ul{margin:0;padding:0}section li{display:inline;display:inline-block;background-color:#fff;position:relative;margin:0 2em 2em 0;vertical-align:top;border:1px solid #ccc;padding:1em 1em 3em 1em;cursor:default}.icon-box{margin:0;width:144px;height:144px;position:relative;background:#ccc url("data:image/gif;base64,R0lGODlhDAAMAIAAAMzMzP///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjEgNjQuMTQwOTQ5LCAyMDEwLzEyLzA3LTEwOjU3OjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93cyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozQjk4OTI0MUY5NTIxMUUyQkJDMEI5NEFEM0Y1QTYwQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozQjk4OTI0MkY5NTIxMUUyQkJDMEI5NEFEM0Y1QTYwQyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjNCOTg5MjNGRjk1MjExRTJCQkMwQjk0QUQzRjVBNjBDIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjNCOTg5MjQwRjk1MjExRTJCQkMwQjk0QUQzRjVBNjBDIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAAwADAAAAhaEH6mHmmzcgzJAUG/NVGrfOZ8YLlABADs=") top left repeat;border:1px solid #ccc;display:table-cell;vertical-align:middle;text-align:center}.icon{display:inline;display:inline-block}h1{margin-top:0}h2{margin:0;padding:0;font-size:1em;font-weight:normal;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;position:absolute;left:1em;right:1em;bottom:1em}footer{display:block;margin:0;padding:0 3em 3em 3em}footer p{margin:0;font-size:.7em}footer a{color:#0f7595;margin-left:0}</style> + +<!-- + +Sprite shape dimensions +==================================================================================================== +You will need to set the sprite shape dimensions via CSS when you use them as inline SVG, otherwise +they would become a huge 100% in size. You may use the following dimension classes for doing so. +They might well be outsourced to an external stylesheet of course. + +--> + +<style type="text/css"> + .svg-clock_o-dims { width: 200px; height: 200px; } + .svg-commit-dims { width: 36px; height: 18px; } + .svg-project-dims { width: 16px; height: 16px; } +</style> +<!-- +==================================================================================================== +--> + + </head> + <body> + +<!-- + +Inline <symbol> SVG sprite +==================================================================================================== +This is an inlined version of the generated SVG sprite. The single images may be <use>d everywhere +below within this document. Please see + + https://github.com/jkphl/svg-sprite/blob/master/docs/configuration.md#defs--symbol-mode + +for further details on how to create this embeddable sprite variant. + +--> + +<svg width="0" height="0" style="position:absolute"> + <symbol viewBox="0 0 1792 1792" id="clock_o" xmlns="http://www.w3.org/2000/svg"><path d="M1024 544v448q0 14-9 23t-23 9H672q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224V544q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5T1281.5 1561 896 1664t-385.5-103T231 1281.5 128 896t103-385.5T510.5 231 896 128t385.5 103T1561 510.5 1664 896z"/></symbol> + <symbol viewBox="0 0 36 18" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M34 7h-7.2c-.9-4-4.5-7-8.8-7s-7.9 3-8.8 7H2C.9 7 0 7.9 0 9s.9 2 2 2h7.2c.9 4 4.5 7 8.8 7s7.9-3 8.8-7H34c1.1 0 2-.9 2-2s-.9-2-2-2m-16 7c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5"/></symbol> + <symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol> +</svg> + +<!-- +==================================================================================================== +--> + + <header> + <h1>SVG <code><symbol></code> sprite preview</h1> + <p>This preview features two methods of using the generated sprite in conjunction with inline SVG. Please have a look at the HTML source for further details and be aware of the following constraints:</p> + <ul> + <li>Your browser has to <a href="http://caniuse.com/#feat=svg-html5" target="_blank">support inline SVG</a> for these techniques to work.</li> + <li>The embedded sprite (A) slightly differs from the generated external one. Please <a href="https://github.com/jkphl/svg-sprite/blob/master/docs/configuration.md#defs--symbol-mode" target="_blank">see the documentation</a> for details on how to create such an embeddable sprite.</li> + <li>Internet Explorer up to version 11 doesn't support external sprites for use with inline SVG. For IE 9-11, you may polyfill this functionality with <a href="https://github.com/jonathantneal/svg4everybody" target="_blank">SVG for Everybody</a>.</li> + </ul> + </header> + <section> + +<!-- + +A) Inline SVG with embedded sprite +==================================================================================================== +These SVG images make use of fragment identifiers (IDs) and are extracted out of the inline sprite +embedded above. They may be styled via CSS. + +--> + + <h3>A) Inline SVG with embedded sprite</h3> + <ul> + + <li title="clock_o"> + <div class="icon-box"> + + <!-- clock_o --> + <svg class="svg-clock_o-dims"> + <use xlink:href="#clock_o"></use> + </svg> + + </div> + <h2>clock_o</h2> + </li> + <li title="commit"> + <div class="icon-box"> + + <!-- commit --> + <svg class="svg-commit-dims"> + <use xlink:href="#commit"></use> + </svg> + + </div> + <h2>commit</h2> + </li> + <li title="project"> + <div class="icon-box"> + + <!-- project --> + <svg class="svg-project-dims"> + <use xlink:href="#project"></use> + </svg> + + </div> + <h2>project</h2> + </li> + </ul> + +<!-- +==================================================================================================== +--> + + </section> + <section> + +<!-- + +B) Inline SVG with external sprite (IE 9-11 with polyfill only) +==================================================================================================== +These SVG images make use of an URL + fragment identifiers (IDs) and refer to the regular external +SVG sprite. They may be styled via CSS. (IE 9-11 with polyfill only) + +--> + + <h3>B) Inline SVG with external sprite (IE 9-11 with polyfill only)</h3> + <ul> + + <li title="clock_o"> + <div class="icon-box"> + + <!-- clock_o --> + <svg class="svg-clock_o-dims"> + <use xlink:href="icons.svg#clock_o"></use> + </svg> + + </div> + <h2>clock_o</h2> + </li> + <li title="commit"> + <div class="icon-box"> + + <!-- commit --> + <svg class="svg-commit-dims"> + <use xlink:href="icons.svg#commit"></use> + </svg> + + </div> + <h2>commit</h2> + </li> + <li title="project"> + <div class="icon-box"> + + <!-- project --> + <svg class="svg-project-dims"> + <use xlink:href="icons.svg#project"></use> + </svg> + + </div> + <h2>project</h2> + </li> + </ul> + +<!-- +==================================================================================================== +--> + + </section> + <footer> + <p>Generated at Fri, 25 Aug 2017 12:38:01 GMT by <a href="https://github.com/jkphl/svg-sprite" target="_blank">svg-sprite</a>.</p> + </footer> + </body> +</html> diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index a07edd264c3..c93e6567baf 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -271,10 +271,6 @@ production: --version="$CI_PIPELINE_ID-$CI_JOB_ID" \ "$name" \ chart/ - - if [[ "$track" == "stable" ]]; then - kubectl rollout status -n "$KUBE_NAMESPACE" -w "deployment/${CI_ENVIRONMENT_SLUG}-auto-deploy" - fi } function install_dependencies() { diff --git a/yarn.lock b/yarn.lock index 02917125f05..ae86887630b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1834,7 +1834,7 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" -document-register-element@^1.3.0: +document-register-element@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940" @@ -2712,6 +2712,10 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +"gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git": + version "1.0.2" + resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#e7621d7b028607ae9c69f8b496cd49b42fe607e4" + glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -5937,6 +5941,10 @@ supports-color@^4.2.1: dependencies: has-flag "^2.0.0" +svg4everybody@2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" |