diff options
533 files changed, 7626 insertions, 2482 deletions
diff --git a/.gitignore b/.gitignore index 65f61e1fade..0696dd217af 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ eslint-report.html package-lock.json /junit_*.xml /coverage-frontend/ +jsdoc/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 45de5ce61c6..68dea56a67d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -499,6 +499,22 @@ rspec-mysql: <<: *rspec-metadata-mysql parallel: 50 +.rspec-quarantine: &rspec-quarantine + script: + - export CACHE_CLASSES=true + - scripts/gitaly-test-spawn + - bin/rspec --color --format documentation --tag quarantine spec/ + +rspec-pg-quarantine: + <<: *rspec-metadata-pg + <<: *rspec-quarantine + allow_failure: true + +rspec-mysql-quarantine: + <<: *rspec-metadata-mysql + <<: *rspec-quarantine + allow_failure: true + static-analysis: <<: *dedicated-no-docs-no-db-pull-cache-job dependencies: @@ -788,6 +804,7 @@ qa:selectors: - bundle exec bin/qa Test::Sanity::Selectors .qa-frontend-node: &qa-frontend-node + <<: *dedicated-no-docs-no-db-pull-cache-job stage: test variables: NODE_OPTIONS: --max_old_space_size=3584 @@ -802,11 +819,6 @@ qa:selectors: - yarn install --frozen-lockfile --cache-folder .yarn-cache - date - yarn run webpack-prod - <<: *except-docs - -qa-frontend-node:6: - <<: *qa-frontend-node - image: node:6-alpine qa-frontend-node:8: <<: *qa-frontend-node @@ -854,6 +866,21 @@ lint:javascript:report: paths: - eslint-report.html +jsdoc: + <<: *dedicated-no-docs-pull-cache-job + stage: post-test + dependencies: + - compile-assets + before_script: [] + script: + - date + - yarn run jsdoc || true # ignore exit code + artifacts: + name: jsdoc + expire_in: 31d + paths: + - jsdoc/ + pages: <<: *dedicated-no-docs-no-db-pull-cache-job before_script: [] @@ -863,6 +890,7 @@ pages: - karma - gitlab:assets:compile - lint:javascript:report + - jsdoc script: - mv public/ .public/ - mkdir public/ @@ -872,6 +900,7 @@ pages: - mv webpack-report/ public/webpack-report/ || true - cp .public/assets/application-*.css public/application.css || true - cp .public/assets/application-*.css.gz public/application.css.gz || true + - mv jsdoc/ public/jsdoc/ || true artifacts: paths: - public diff --git a/.gitlab/issue_templates/Add style proposal b/.gitlab/issue_templates/Add style proposal.md index 1a3be44bea0..1a3be44bea0 100644 --- a/.gitlab/issue_templates/Add style proposal +++ b/.gitlab/issue_templates/Add style proposal.md diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 639a236631d..4d4d3bfda15 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -4,7 +4,7 @@ ### Target audience -<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst" --> +<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst". Use the persona labels as well https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A --> ### Further details @@ -12,12 +12,12 @@ ### Proposal -<!--- How are we going to solve the problem? --> +<!--- How are we going to solve the problem? Try to include the user journey! --> ### What does success look like, and how can we measure that? -<!--- If no way to measure success, link to an issue that will implement a way to measure this --> +<!--- Define both the success metrics and acceptance criteria. Note thet success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this --> ### Links / references -/label ~"feature proposal" +/label ~feature diff --git a/.gitlab/issue_templates/Security Release.md b/.gitlab/issue_templates/Security Release.md new file mode 100644 index 00000000000..1734e915ad2 --- /dev/null +++ b/.gitlab/issue_templates/Security Release.md @@ -0,0 +1,69 @@ +<!-- +# Read me first! + +Set the title to: `Security Release: 11.4.X, 11.3.X, and 11.2.X` +--> + +## Releases tasks + +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/release-manager.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/security-engineer.md + +## Version issues: + +* 11.4.X: {release task link} +* 11.3.X: {release task link} +* 11.2.X: {release task link} + +## Security Issues: + +### CE + +* {https://gitlab.com/gitlab-org/gitlab-ce/issues link} + +### EE + +* {https://gitlab.com/gitlab-org/gitlab-ee/issues link} + +## Security Issues in dev.gitlab.org: + +### CE + +- {https://dev.gitlab.org/gitlab/gitlabhq/issues link} + +| Version | MR | Status| +|---------|----|-------| +| 11.4 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| master | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | + + + +### EE + +* {https://dev.gitlab.org/gitlab/gitlabhq/issues/ link} + + +| Version | MR | Status| +|---------|----|-------| +| 11.4| {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| master | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | + + +## QA +{QA issue link} + +## Blog post + +Dev: {https://dev.gitlab.org/gitlab/www-gitlab-com/merge_requests/ link}<br/> +gitlab.com: {https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ link} + +## Email notification +{https://gitlab.com/gitlab-com/marketing/general/issues/ link} + +/label ~security +/confidential diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 847a0f74aa2..f2ba9fdb174 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -686,17 +686,6 @@ Style/TrailingUnderscoreVariable: - 'spec/lib/gitlab/etag_caching/middleware_spec.rb' - 'spec/services/quick_actions/interpret_service_spec.rb' -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. -# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - 'app/models/external_issue.rb' - - 'app/serializers/base_serializer.rb' - - 'lib/gitlab/auth/ldap/person.rb' - - 'lib/system_check/base_check.rb' - # Offense count: 4 # Cop supports --auto-correct. Style/UnlessElse: diff --git a/CHANGELOG.md b/CHANGELOG.md index e86c818298b..b47dc4e19ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.6.5 (2019-01-17) + +### Fixed (5 changes) + +- Add syntax highlighting to suggestion diff. !24156 +- Fix broken templated "Too many changes to show" text. !24282 +- Fix requests profiler in admin page not rendering HTML properly. !24291 +- Fix no avatar not showing in user selection box. !24346 +- Fixed diff suggestions removing dashes. + + +## 11.6.4 (2019-01-15) + +### Security (1 change) + +- Validate bundle files before unpacking them. + + ## 11.6.3 (2019-01-04) ### Fixed (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0eed1a29efd..f88cf52e6ef 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.12.0 +1.13.0
\ No newline at end of file diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 3a3cd8cc8b0..88c5fb891dc 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.3.1 +1.4.0 @@ -125,9 +125,9 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.8' gem 'asciidoctor-plantuml', '0.0.8' gem 'rouge', '~> 3.1' -gem 'truncato', '~> 0.7.9' +gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 2.7.0' -gem 'nokogiri', '~> 1.8.5' +gem 'nokogiri', '~> 1.10.1' gem 'escape_utils', '~> 1.1' # Calendar rendering @@ -160,12 +160,12 @@ gem 'acts-as-taggable-on', '~> 5.0' # Background jobs gem 'sidekiq', '~> 5.2.1' -gem 'sidekiq-cron', '~> 0.6.0' +gem 'sidekiq-cron', '~> 1.0' gem 'redis-namespace', '~> 1.6.0' gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch' # Cron Parser -gem 'rufus-scheduler', '~> 3.4' +gem 'fugit', '~> 1.1' # HTTP requests gem 'httparty', '~> 0.13.3' @@ -304,6 +304,12 @@ group :metrics do gem 'raindrops', '~> 0.18' end +group :tracing do + # OpenTracing + gem 'opentracing', '~> 0.4.3' + gem 'jaeger-client', '~> 0.10.0' +end + group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false diff --git a/Gemfile.lock b/Gemfile.lock index b4602dbbf36..419a6831924 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -185,7 +185,7 @@ GEM erubi (1.7.1) erubis (2.7.0) escape_utils (1.2.1) - et-orbi (1.0.3) + et-orbi (1.1.7) tzinfo eventmachine (1.2.7) excon (0.62.0) @@ -206,7 +206,7 @@ GEM fast_blank (1.0.0) fast_gettext (1.6.0) ffaker (2.10.0) - ffi (1.9.25) + ffi (1.10.0) flipper (0.13.0) flipper-active_record (0.13.0) activerecord (>= 3.2, < 6) @@ -258,6 +258,9 @@ GEM foreman (0.84.0) thor (~> 0.19.1) formatador (0.2.5) + fugit (1.1.7) + et-orbi (~> 1.1, >= 1.1.7) + raabro (~> 1.1) fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) @@ -282,7 +285,7 @@ GEM gitlab-markup (1.6.5) gitlab-sidekiq-fetcher (0.4.0) sidekiq (~> 5) - gitlab-styles (2.4.1) + gitlab-styles (2.5.1) rubocop (~> 0.54.0) rubocop-gitlab-security (~> 0.1.0) rubocop-rspec (~> 1.19) @@ -389,6 +392,9 @@ GEM cause json ipaddress (0.8.3) + jaeger-client (0.10.0) + opentracing (~> 0.3) + thrift jira-ruby (1.4.1) activesupport multipart-post @@ -462,9 +468,9 @@ GEM mimemagic (0.3.2) mini_magick (4.8.0) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) - msgpack (1.2.4) + msgpack (1.2.6) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -477,8 +483,8 @@ GEM net-ssh (5.0.1) netrc (0.11.0) nio4r (2.3.1) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) nokogiri numerizer (0.1.1) @@ -544,6 +550,8 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) + opentracing (0.4.3) + optimist (3.0.0) org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) @@ -606,6 +614,7 @@ GEM get_process_mem (~> 0.2) puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) + raabro (1.1.6) rack (2.0.6) rack-accept (0.4.5) rack (>= 0.4) @@ -664,10 +673,10 @@ GEM ffi (>= 0.5.0, < 2) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) - rbtrace (0.4.10) + rbtrace (0.4.11) ffi (>= 1.0.6) msgpack (>= 0.4.3) - trollop (>= 1.16.2) + optimist (>= 3.0.0) rdoc (6.0.4) re2 (1.1.1) recaptcha (3.0.0) @@ -775,8 +784,6 @@ GEM rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.2) - rufus-scheduler (3.4.0) - et-orbi (~> 1.0) rugged (0.27.5) safe_yaml (1.0.4) sanitize (4.6.6) @@ -820,8 +827,8 @@ GEM connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) - sidekiq-cron (0.6.0) - rufus-scheduler (>= 3.3.0) + sidekiq-cron (1.0.4) + fugit (~> 1.1) sidekiq (>= 4.2.1) signet (0.11.0) addressable (~> 2.3) @@ -868,6 +875,7 @@ GEM rack (>= 1, < 3) thor (0.19.4) thread_safe (0.3.6) + thrift (0.11.0.0) tilt (2.0.8) timecop (0.8.1) timfel-krb5-auth (0.8.3) @@ -875,10 +883,9 @@ GEM parslet (~> 1.8.0) toml-rb (1.0.0) citrus (~> 3.0, > 3.0) - trollop (2.1.3) - truncato (0.7.10) + truncato (0.7.11) htmlentities (~> 4.3.1) - nokogiri (~> 1.8.0, >= 1.7.0) + nokogiri (>= 1.7.0, <= 2.0) tzinfo (1.2.5) thread_safe (~> 0.1) u2f (0.2.1) @@ -1003,6 +1010,7 @@ DEPENDENCIES fog-rackspace (~> 0.1.1) font-awesome-rails (~> 4.7) foreman (~> 0.84.0) + fugit (~> 1.1) fuubar (~> 2.2.0) gemojione (~> 3.3) gettext (~> 3.2.2) @@ -1037,6 +1045,7 @@ DEPENDENCIES httparty (~> 0.13.3) icalendar influxdb (~> 0.2) + jaeger-client (~> 0.10.0) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) js_regex (~> 2.2.1) @@ -1059,7 +1068,7 @@ DEPENDENCIES nakayoshi_fork (~> 0.0.4) net-ldap net-ssh (~> 5.0) - nokogiri (~> 1.8.5) + nokogiri (~> 1.10.1) oauth2 (~> 1.4) octokit (~> 4.9) omniauth (~> 1.8) @@ -1077,6 +1086,7 @@ DEPENDENCIES omniauth-shibboleth (~> 1.3.0) omniauth-twitter (~> 1.4) omniauth_crowd (~> 2.2.0) + opentracing (~> 0.4.3) org-ruby (~> 0.9.12) peek (~> 1.0.1) peek-gc (~> 0.0.2) @@ -1127,7 +1137,6 @@ DEPENDENCIES ruby-prof (~> 0.17.0) ruby-progressbar ruby_parser (~> 3.8) - rufus-scheduler (~> 3.4) rugged (~> 0.27) sanitize (~> 4.6) sass (~> 3.5) @@ -1141,7 +1150,7 @@ DEPENDENCIES sham_rack (~> 1.3.6) shoulda-matchers (~> 3.1.2) sidekiq (~> 5.2.1) - sidekiq-cron (~> 0.6.0) + sidekiq-cron (~> 1.0) simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) @@ -1156,7 +1165,7 @@ DEPENDENCIES thin (~> 1.7.0) timecop (~> 0.8.0) toml-rb (~> 1.0.0) - truncato (~> 0.7.9) + truncato (~> 0.7.11) u2f (~> 0.2.1) uglifier (~> 2.7.2) unf (~> 0.1.4) diff --git a/PROCESS.md b/PROCESS.md index f2eed5544fc..7fdac098807 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -56,7 +56,7 @@ Below we describe the contributing process to GitLab for two reasons: Several people from the [GitLab team][team] are helping community members to get their contributions accepted by meeting our [Definition of done][done]. -What you can expect from them is described at https://about.gitlab.com/roles/merge-request-coach/. +What you can expect from them is described at https://about.gitlab.com/job-families/expert/merge-request-coach/. ### Milestones on community contribution issues diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index 9051be1e102..cad5611c8c5 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -55,7 +55,7 @@ export default { :disabled="badge.isDeleting" class="btn btn-default append-right-8" type="button" - @click="editBadge(badge);" + @click="editBadge(badge)" > <icon :size="16" :aria-label="__('Edit')" name="pencil" /> </button> @@ -65,7 +65,7 @@ export default { type="button" data-toggle="modal" data-target="#delete-badge-modal" - @click="updateBadgeInModal(badge);" + @click="updateBadgeInModal(badge)" > <icon :size="16" :aria-label="__('Delete')" name="remove" /> </button> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 30fbdb9e97f..f569322ab70 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -86,7 +86,7 @@ export default { class="board-card" @mousedown="mouseDown" @mousemove="mouseMove" - @mouseup="showIssue($event);" + @mouseup="showIssue($event)" > <issue-card-inner :list="list" diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index f3f341ece5c..a689dfc3768 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -221,7 +221,7 @@ export default { </script> <template> - <div class="board-list-component"> + <div class="board-list-component d-flex flex-column"> <div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues"> <gl-loading-icon /> </div> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 93bcb4e129e..28d96dab605 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -96,7 +96,7 @@ export default { <template> <div class="board-new-issue-form"> <div class="board-card"> - <form @submit="submit($event);"> + <form @submit="submit($event)"> <div v-if="error" class="flash-container"> <div class="flash-alert">An error occurred. Please try again.</div> </div> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 0f581c3d37d..90ab3a76342 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -184,7 +184,7 @@ export default { :title="label.description" class="badge color-label append-right-4 prepend-top-4" type="button" - @click="filterByLabel(label);" + @click="filterByLabel(label)" > {{ label.title }} </button> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index defd857b92c..2a0008467c4 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -58,7 +58,7 @@ export default { v-if="activeTab === 'selected'" class="btn btn-default" type="button" - @click="changeTab('all');" + @click="changeTab('all')" > Open issues </button> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index b1bc7d87086..d4afd9d59da 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -71,7 +71,7 @@ export default { <span class="inline add-issues-footer-to-list"> to list </span> <lists-dropdown /> </div> - <button class="btn btn-default float-right" type="button" @click="toggleModal(false);"> + <button class="btn btn-default float-right" type="button" @click="toggleModal(false)"> Cancel </button> </footer> diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index d0e285a149e..1f0961e02d8 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -58,7 +58,7 @@ export default { class="close" data-dismiss="modal" aria-label="Close" - @click="toggleModal(false);" + @click="toggleModal(false)" > <span aria-hidden="true">×</span> </button> diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index 878bb002c6c..e9ed2de859d 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -130,7 +130,7 @@ export default { <div :class="{ 'is-active': issue.selected }" class="board-card" - @click="toggleIssue($event, issue);" + @click="toggleIssue($event, issue)" > <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" /> <icon diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 820d0679df5..3fbe8fe1be7 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -38,7 +38,7 @@ export default { :class="{ 'is-active': list.id == selected.id }" href="#" role="button" - @click.prevent="modal.selectedList = list;" + @click.prevent="modal.selectedList = list" > <span :style="{ backgroundColor: list.label.color }" class="dropdown-label-box"> </span> {{ list.title }} diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue index 7b800a6ab97..2d2920e312e 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.vue +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -21,12 +21,12 @@ export default { <div class="top-area prepend-top-10 append-bottom-10"> <ul class="nav-links issues-state-filters"> <li :class="{ active: activeTab == 'all' }"> - <a href="#" role="button" @click.prevent="changeTab('all');"> + <a href="#" role="button" @click.prevent="changeTab('all')"> Open issues <span class="badge badge-pill"> {{ issuesCount }} </span> </a> </li> <li :class="{ active: activeTab == 'selected' }"> - <a href="#" role="button" @click.prevent="changeTab('selected');"> + <a href="#" role="button" @click.prevent="changeTab('selected')"> Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span> </a> </li> diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index d899b7fbd8c..8274647744f 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -82,7 +82,7 @@ export default { <template> <div> <label class="label-bold prepend-top-10"> Project </label> - <div ref="projectsDropdown" class="dropdown"> + <div ref="projectsDropdown" class="dropdown dropdown-projects"> <button class="dropdown-menu-toggle wide" type="button" diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 10f02739ec8..50efecb3475 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -13,6 +13,9 @@ export default class ContextualSidebar { initDomElements() { this.$page = $('.layout-page'); this.$sidebar = $('.nav-sidebar'); + + if (!this.$sidebar.length) return; + this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$overlay = $('.mobile-overlay'); this.$openSidebar = $('.toggle-mobile-nav'); @@ -21,12 +24,14 @@ export default class ContextualSidebar { } bindEvents() { + if (!this.$sidebar.length) return; + document.addEventListener('click', e => { if ( !e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md') ) { - this.toggleCollapsedSidebar(true); + this.toggleCollapsedSidebar(true, true); } }); this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); @@ -34,7 +39,7 @@ export default class ContextualSidebar { this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$sidebarToggle.on('click', () => { const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop'); - this.toggleCollapsedSidebar(value); + this.toggleCollapsedSidebar(value, true); }); $(window).on('resize', () => _.debounce(this.render(), 100)); @@ -53,16 +58,19 @@ export default class ContextualSidebar { this.$sidebar.removeClass('sidebar-collapsed-desktop'); } - toggleCollapsedSidebar(collapsed) { + toggleCollapsedSidebar(collapsed, saveCookie) { const breakpoint = bp.getBreakpointSize(); if (this.$sidebar.length) { this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } - ContextualSidebar.setCollapsedCookie(collapsed); - this.toggleSidebarOverflow(); + if (saveCookie) { + ContextualSidebar.setCollapsedCookie(collapsed); + } + + requestIdleCallback(() => this.toggleSidebarOverflow()); } toggleSidebarOverflow() { @@ -74,13 +82,15 @@ export default class ContextualSidebar { } render() { + if (!this.$sidebar.length) return; + const breakpoint = bp.getBreakpointSize(); if (breakpoint === 'sm' || breakpoint === 'md') { - this.toggleCollapsedSidebar(true); + this.toggleCollapsedSidebar(true, false); } else if (breakpoint === 'lg') { const collapse = parseBoolean(Cookies.get('sidebar_collapsed')); - this.toggleCollapsedSidebar(collapse); + this.toggleCollapsedSidebar(collapse, false); } } } diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index f75345d31f8..f0a827be7e8 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -54,6 +54,9 @@ export default { showDropdowns() { return !this.commit && this.mergeRequestDiffs.length; }, + baseVersionPath() { + return this.mergeRequestDiff.base_version_path; + }, }, methods: { ...mapActions('diffs', [ @@ -95,6 +98,7 @@ export default { and <compare-versions-dropdown :other-versions="comparableDiffs" + :base-version-path="baseVersionPath" :start-version="startVersion" :target-branch="targetBranch" class="mr-version-compare-dropdown" diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index b9b1ee02697..561188c1e8f 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -34,14 +34,13 @@ export default { required: false, default: false, }, + baseVersionPath: { + type: String, + required: false, + default: null, + }, }, computed: { - baseVersion() { - return { - name: 'hii', - versionIndex: -1, - }; - }, targetVersions() { if (this.mergeRequestVersion) { return this.otherVersions; @@ -62,6 +61,9 @@ export default { ); }, href(version) { + if (this.isBase(version)) { + return this.baseVersionPath; + } if (this.showCommitCount) { return version.version_path; } diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index ba6dcd63880..6dc2f5d3f68 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -127,7 +127,7 @@ export default { :save-button-title="__('Comment')" class="diff-comment-form new-note discussion-form discussion-form-container" @handleFormUpdate="handleSaveNote" - @cancelForm="closeDiffFileCommentForm(diffFile.file_hash);" + @cancelForm="closeDiffFileCommentForm(diffFile.file_hash)" /> </div> </diff-viewer> diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue index b2021cd6061..4c73eea4049 100644 --- a/app/assets/javascripts/diffs/components/diff_discussions.vue +++ b/app/assets/javascripts/diffs/components/diff_discussions.vue @@ -68,7 +68,7 @@ export default { }" type="button" class="js-diff-notes-toggle" - @click="toggleDiscussion({ discussionId: discussion.id });" + @click="toggleDiscussion({ discussionId: discussion.id })" > <icon v-if="discussion.expanded" name="collapse" class="collapse-icon" /> <template v-else> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index f75a01b023b..b58f704bebb 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -145,7 +145,7 @@ export default { <div ref="header" class="js-file-title file-title file-title-flex-parent" - @click="handleToggleFile($event, true);" + @click="handleToggleFile($event, true)" > <div class="file-header-content"> <icon diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index c0613d80d37..6709df48637 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -179,7 +179,7 @@ export default { v-if="lineNumber" :data-linenumber="lineNumber" :href="lineHref" - @click="setHighlightedRow(lineCode);" + @click="setHighlightedRow(lineCode)" > </a> <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index e7569ba7b84..18edbe286ba 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -28,6 +28,11 @@ export default { type: Object, required: true, }, + helpPagePath: { + type: String, + required: false, + default: '', + }, }, computed: { ...mapState({ @@ -95,6 +100,7 @@ export default { :is-editing="true" :line-code="line.line_code" :line="line" + :help-page-path="helpPagePath" save-button-title="Comment" class="diff-comment-form" @cancelForm="handleCancelCommentForm" diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue index d30e64312aa..4a83c5a72a5 100644 --- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue +++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue @@ -97,7 +97,7 @@ export default { v-if="canComment" type="button" class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button" - @click="clickedImage($event.offsetX, $event.offsetY);" + @click="clickedImage($event.offsetX, $event.offsetY)" > <span class="sr-only"> {{ __('Add image comment') }} </span> </button> @@ -109,7 +109,7 @@ export default { :disabled="!shouldToggleDiscussion" class="js-image-badge" type="button" - @click="toggleDiscussion({ discussionId: discussion.id });" + @click="toggleDiscussion({ discussionId: discussion.id })" > <icon v-if="showCommentIcon" name="image-comment-dark" /> <template v-else> diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index 814ee0b7c02..69146f1f6fd 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -54,6 +54,7 @@ export default { :diff-file-hash="diffFileHash" :line="line" :note-target-line="line" + :help-page-path="helpPagePath" /> </div> </td> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index a65cf025cde..370cb6e339a 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -101,6 +101,7 @@ export default { :diff-file-hash="diffFileHash" :line="line.left" :note-target-line="line.left" + :help-page-path="helpPagePath" line-position="left" /> </td> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index eb8f274aff3..097587c5ac1 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -81,7 +81,7 @@ export default { :placeholder="s__('MergeRequest|Filter files')" type="search" class="form-control" - @focus="toggleFocusSearch(true);" + @focus="toggleFocusSearch(true)" @blur="blurSearch" /> <button @@ -104,7 +104,7 @@ export default { }" class="btn btn-default pt-0 pb-0 d-flex align-items-center" type="button" - @click="toggleRenderTreeList(false);" + @click="toggleRenderTreeList(false)" > <icon name="hamburger" /> </button> @@ -117,7 +117,7 @@ export default { }" class="btn btn-default pt-0 pb-0 d-flex align-items-center" type="button" - @click="toggleRenderTreeList(true);" + @click="toggleRenderTreeList(true)" > <icon name="file-tree" /> </button> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 78a39baa4cb..0af1ba13d36 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -32,3 +32,5 @@ export const LINES_TO_BE_RENDERED_DIRECTLY = 100; export const MAX_LINES_TO_BE_RENDERED = 2000; export const MR_TREE_SHOW_KEY = 'mr_tree_show'; + +export const TREE_TYPE = 'tree'; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 00a4bb6d3a3..196c9dfb1c2 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -5,6 +5,7 @@ import createFlash from '~/flash'; import { s__ } from '~/locale'; import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; +import TreeWorker from '../workers/tree_worker'; import eventHub from '../../notes/event_hub'; import { getDiffPositionByLineCode, getNoteFormData } from './utils'; import * as types from './mutation_types'; @@ -21,17 +22,29 @@ export const setBaseConfig = ({ commit }, options) => { }; export const fetchDiffFiles = ({ state, commit }) => { + const worker = new TreeWorker(); + commit(types.SET_LOADING, true); + worker.addEventListener('message', ({ data }) => { + commit(types.SET_TREE_DATA, data); + + worker.terminate(); + }); + return axios .get(state.endpoint) .then(res => { commit(types.SET_LOADING, false); commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []); commit(types.SET_DIFF_DATA, res.data); + + worker.postMessage(state.diffFiles); + return Vue.nextTick(); }) - .then(handleLocationHash); + .then(handleLocationHash) + .catch(() => worker.terminate()); }; export const setHighlightedRow = ({ commit }, lineCode) => { diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 0338cde3658..6ed8c5709a8 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -18,3 +18,5 @@ export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM'; export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM'; export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM'; export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW'; + +export const SET_TREE_DATA = 'SET_TREE_DATA'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index ed4203cf5e0..00095997ba3 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,5 +1,4 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { sortTree } from '~/ide/stores/utils'; import { findDiffFile, addLineReferences, @@ -7,7 +6,6 @@ import { addContextLines, prepareDiffData, isDiscussionApplicableToLine, - generateTreeList, } from './utils'; import * as types from './mutation_types'; @@ -23,12 +21,9 @@ export default { [types.SET_DIFF_DATA](state, data) { prepareDiffData(data); - const { tree, treeEntries } = generateTreeList(data.diff_files); Object.assign(state, { ...convertObjectPropsToCamelCase(data), - tree: sortTree(tree), - treeEntries, }); }, @@ -239,4 +234,8 @@ export default { [types.SET_HIGHLIGHTED_ROW](state, lineCode) { state.highlightedRow = lineCode; }, + [types.SET_TREE_DATA](state, { treeEntries, tree }) { + state.treeEntries = treeEntries; + state.tree = tree; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index f427367c11e..ada93b570b0 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -11,6 +11,7 @@ import { MATCH_LINE_TYPE, LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED, + TREE_TYPE, } from '../constants'; export function findDiffFile(files, hash) { @@ -289,8 +290,63 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD return latestDiff && discussion.active && line_code === discussion.line_code; } -export const generateTreeList = files => - files.reduce( +export const getLowestSingleFolder = folder => { + const getFolder = (blob, start = []) => + blob.tree.reduce( + (acc, file) => { + const shouldGetFolder = file.tree.length === 1 && file.tree[0].type === TREE_TYPE; + const currentFileTypeTree = file.type === TREE_TYPE; + const path = shouldGetFolder || currentFileTypeTree ? acc.path.concat(file.name) : acc.path; + const tree = shouldGetFolder || currentFileTypeTree ? acc.tree.concat(file) : acc.tree; + + if (shouldGetFolder) { + const firstFolder = getFolder(file); + + path.push(firstFolder.path); + tree.push(...firstFolder.tree); + } + + return { + ...acc, + path, + tree, + }; + }, + { path: start, tree: [] }, + ); + const { path, tree } = getFolder(folder, [folder.name]); + + return { + path: path.join('/'), + treeAcc: tree.length ? tree[tree.length - 1].tree : null, + }; +}; + +export const flattenTree = tree => { + const flatten = blobTree => + blobTree.reduce((acc, file) => { + const blob = file; + let treeToFlatten = blob.tree; + + if (file.type === TREE_TYPE && file.tree.length === 1) { + const { treeAcc, path } = getLowestSingleFolder(file); + + if (treeAcc) { + blob.name = path; + treeToFlatten = flatten(treeAcc); + } + } + + blob.tree = flatten(treeToFlatten); + + return acc.concat(blob); + }, []); + + return flatten(tree); +}; + +export const generateTreeList = files => { + const { treeEntries, tree } = files.reduce( (acc, file) => { const split = file.new_path.split('/'); @@ -335,6 +391,9 @@ export const generateTreeList = files => { treeEntries: {}, tree: [] }, ); + return { treeEntries, tree: flattenTree(tree) }; +}; + export const getDiffMode = diffFile => { const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]); return ( diff --git a/app/assets/javascripts/diffs/workers/tree_worker.js b/app/assets/javascripts/diffs/workers/tree_worker.js new file mode 100644 index 00000000000..534d737c77e --- /dev/null +++ b/app/assets/javascripts/diffs/workers/tree_worker.js @@ -0,0 +1,14 @@ +import { sortTree } from '~/ide/stores/utils'; +import { generateTreeList } from '../store/utils'; + +// eslint-disable-next-line no-restricted-globals +self.addEventListener('message', e => { + const { data } = e; + const { treeEntries, tree } = generateTreeList(data); + + // eslint-disable-next-line no-restricted-globals + self.postMessage({ + treeEntries, + tree: sortTree(tree), + }); +}); diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 1f7dab9fbd2..208bd19f6b0 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -92,7 +92,7 @@ export default { :disabled="isActionDisabled(action)" type="button" class="js-manual-action-link no-btn btn d-flex align-items-center" - @click="onClickAction(action);" + @click="onClickAction(action)" > <span class="flex-fill"> {{ action.name }} </span> <span v-if="action.scheduledAt" class="text-secondary"> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index ae9459a2482..87c1d44dd40 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -96,9 +96,9 @@ export default { <tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" /> <div v-if="canCreateEnvironment && !isLoading" class="nav-controls"> - <a :href="newEnvironmentPath" class="btn btn-success"> - {{ s__('Environments|New environment') }} - </a> + <a :href="newEnvironmentPath" class="btn btn-success">{{ + s__('Environments|New environment') + }}</a> </div> </div> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 533e90e2222..75bdf87137f 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -85,9 +85,9 @@ export default { <div :key="`sub-div-${i}`"> <div class="text-center prepend-top-10"> - <a :href="folderUrl(model)" class="btn btn-default"> - {{ s__('Environments|Show all') }} - </a> + <a :href="folderUrl(model)" class="btn btn-default">{{ + s__('Environments|Show all') + }}</a> </div> </div> </template> diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 3cf6e4ad14d..982e550e73c 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -15,11 +15,11 @@ export default () => const environmentsData = document.querySelector(this.$options.el).dataset; return { - endpoint: environmentsData.endpoint, - folderName: environmentsData.folderName, + endpoint: environmentsData.environmentsDataEndpoint, + folderName: environmentsData.environmentsDataFolderName, cssContainerClass: environmentsData.cssClass, - canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment), - canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), + canCreateDeployment: parseBoolean(environmentsData.environmentsDataCanCreateDeployment), + canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment), }; }, render(createElement) { diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js index 808ae2c9a41..3d609448efe 100644 --- a/app/assets/javascripts/error_tracking/index.js +++ b/app/assets/javascripts/error_tracking/index.js @@ -4,10 +4,6 @@ import store from './store'; import ErrorTrackingList from './components/error_tracking_list.vue'; export default () => { - if (!gon.features.errorTracking) { - return; - } - // eslint-disable-next-line no-new new Vue({ el: '#js-error_tracking', diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index 6b1a934d3fe..19bc3313373 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -66,7 +66,7 @@ export default { <button type="button" class="filtered-search-history-dropdown-item" - @click="onItemActivated(item.text);" + @click="onItemActivated(item.text)" > <span> <span @@ -88,7 +88,7 @@ export default { <button type="button" class="filtered-search-history-clear-button" - @click="onRequestClearRecentSearches($event);" + @click="onRequestClearRecentSearches($event)" > Clear recent searches </button> diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 3ac00c51df4..2b6af9060d1 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -24,6 +24,9 @@ export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); let headerHeight = 50; export const getHeaderHeight = () => headerHeight; +const setHeaderHeight = () => { + headerHeight = sidebar.offsetTop; +}; export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); @@ -186,7 +189,7 @@ export default () => { }); } - headerHeight = document.querySelector('.nav-sidebar').offsetTop; + requestIdleCallback(setHeaderHeight); items.forEach(el => { const subItems = el.querySelector('.sidebar-sub-level-items'); diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index 63531f1f246..968e255e1fc 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -47,6 +47,12 @@ export default { } eventHub.$on(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler); + + // As we init it through requestIdleCallback it could be that the dropdown is already open + const namespaceDropdown = document.getElementById(`nav-${this.namespace}-dropdown`); + if (namespaceDropdown && namespaceDropdown.classList.contains('show')) { + this.dropdownOpenHandler(); + } }, beforeDestroy() { eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler); diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js index 5157ff211dc..6263acbab8e 100644 --- a/app/assets/javascripts/frequent_items/index.js +++ b/app/assets/javascripts/frequent_items/index.js @@ -17,7 +17,7 @@ const frequentItemDropdowns = [ }, ]; -document.addEventListener('DOMContentLoaded', () => { +const initFrequentItemDropdowns = () => { frequentItemDropdowns.forEach(dropdown => { const { namespace, key } = dropdown; const el = document.getElementById(`js-${namespace}-dropdown`); @@ -66,4 +66,8 @@ document.addEventListener('DOMContentLoaded', () => { }, }); }); +}; + +document.addEventListener('DOMContentLoaded', () => { + requestIdleCallback(initFrequentItemDropdowns); }); diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index a1f66ff764d..7c769ab7fa0 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -45,7 +45,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-edit-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.edit);" + @click.prevent="changedActivityView($event, $options.activityBarViews.edit)" > <icon name="code" /> </button> @@ -62,7 +62,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-review-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.review);" + @click.prevent="changedActivityView($event, $options.activityBarViews.review)" > <icon name="file-modified" /> </button> @@ -79,7 +79,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-commit-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.commit);" + @click.prevent="changedActivityView($event, $options.activityBarViews.commit)" > <icon name="commit" /> </button> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue index 6f1ded91753..00b2d236da3 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -111,8 +111,8 @@ export default { name="commit-message" @scroll="handleScroll" @input="onInput" - @focus="updateIsFocused(true);" - @blur="updateIsFocused(false);" + @focus="updateIsFocused(true)" + @blur="updateIsFocused(false)" @keydown.ctrl.enter="onCtrlEnter" @keydown.meta.enter="onCtrlEnter" > diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 3525084b1cb..2b44438f849 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -65,7 +65,7 @@ export default { :disabled="disabled" type="radio" name="commit-action" - @change="updateCommitAction($event.target.value);" + @change="updateCommitAction($event.target.value)" /> <span class="prepend-left-10"> <span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot> @@ -76,7 +76,7 @@ export default { :placeholder="newBranchName" type="text" class="form-control monospace" - @input="updateBranchName($event.target.value);" + @input="updateBranchName($event.target.value)" /> </div> </fieldset> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index 02c2004d495..e054be86c1e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -48,7 +48,7 @@ export default { data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop.prevent="stageChange(path);" + @click.stop.prevent="stageChange(path)" > <icon :size="16" name="mobile-issue-close" class="ml-auto mr-auto" /> </button> @@ -70,7 +70,7 @@ export default { :header-title-text="modalTitle" :footer-primary-button-text="__('Discard changes')" footer-primary-button-variant="danger" - @submit="discardFileChanges(path);" + @submit="discardFileChanges(path)" > {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }} </gl-modal> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue index ce41fcdb087..0567ef54ff3 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -33,7 +33,7 @@ export default { data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop.prevent="unstageChange(path);" + @click.stop.prevent="unstageChange(path)" > <icon :size="16" name="redo" class="ml-auto mr-auto" /> </button> diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index 5f99261ec39..732fa0786b0 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -40,7 +40,7 @@ export default { 'is-active': viewer === $options.viewerTypes.mr, }" href="#" - @click.prevent="changeMode($options.viewerTypes.mr);" + @click.prevent="changeMode($options.viewerTypes.mr)" > <strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} </strong> <span class="dropdown-menu-inner-content"> @@ -54,7 +54,7 @@ export default { 'is-active': viewer === $options.viewerTypes.diff, }" href="#" - @click.prevent="changeMode($options.viewerTypes.diff);" + @click.prevent="changeMode($options.viewerTypes.diff)" > <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> <span class="dropdown-menu-inner-content"> diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index bb391912572..0b0cd7b75eb 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -164,7 +164,7 @@ export default { </script> <template> - <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false);"> + <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false)"> <div class="dropdown-menu diff-file-changes ide-file-finder show"> <div class="dropdown-input"> <input @@ -174,8 +174,8 @@ export default { type="search" class="dropdown-input-field" autocomplete="off" - @keydown="onKeydown($event);" - @keyup="onKeyup($event);" + @keydown="onKeydown($event)" + @keyup="onKeyup($event)" /> <i :class="{ diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue index 414ea9c7d4d..343e0cca672 100644 --- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -91,7 +91,7 @@ export default { <gl-loading-icon v-if="showLoading" :size="2" /> <ul v-else> <li v-for="(item, index) in outputData" :key="index"> - <button type="button" @click="clickItem(item);">{{ item.name }}</button> + <button type="button" @click="clickItem(item)">{{ item.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index e2e0acc22b1..f1d40586903 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -84,7 +84,7 @@ export default { <button type="button" class="p-0 border-0 h-50" - @click="openRightPane($options.rightSidebarViews.pipelines);" + @click="openRightPane($options.rightSidebarViews.pipelines)" > <ci-icon v-tooltip diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 9fc21adae7c..f93496132a4 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -43,7 +43,7 @@ export default { :show-label="false" class="d-flex border-0 p-0 mr-3 qa-new-file" icon="doc-new" - @click="openNewEntryModal({ type: 'blob' });" + @click="openNewEntryModal({ type: 'blob' })" /> <upload :show-label="false" @@ -56,7 +56,7 @@ export default { :show-label="false" class="d-flex border-0 p-0" icon="folder-new" - @click="openNewEntryModal({ type: 'tree' });" + @click="openNewEntryModal({ type: 'tree' })" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue index e8fe5fc696d..7710bfb49ec 100644 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -75,7 +75,7 @@ export default { <template> <div class="ide-pipeline build-page d-flex flex-column flex-fill"> <header class="ide-job-header d-flex align-items-center"> - <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null);"> + <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null)"> <icon name="chevron-left" /> {{ __('View jobs') }} </button> </header> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index ac2b0eddfb4..2d55ffb3c65 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -84,7 +84,7 @@ export default { :placeholder="__('Search merge requests')" @focus="onSearchFocus" @input="searchMergeRequests" - @removeToken="setSearchType(null);" + @removeToken="setSearchType(null)" /> <icon :size="18" name="search" class="input-icon" /> </div> @@ -102,7 +102,7 @@ export default { <button type="button" class="btn-link d-flex align-items-center" - @click.stop="setSearchType(searchType);" + @click.stop="setSearchType(searchType)" > <span class="d-flex append-right-default ide-search-list-current-icon"> <icon :size="18" name="search" /> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index a50d729036f..d7a7b1b4d78 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -73,7 +73,7 @@ export default { :aria-label="__('Create new file or directory')" type="button" class="rounded border-0 d-flex ide-entry-dropdown-toggle" - @click.stop="openDropdown();" + @click.stop="openDropdown()" > <icon name="ellipsis_v" /> <icon name="arrow-down" /> </button> @@ -85,7 +85,7 @@ export default { class="d-flex" icon="doc-new" icon-classes="mr-2" - @click="createNewItem('blob');" + @click="createNewItem('blob')" /> </li> <li><upload :path="path" @create="createTempEntry" /></li> @@ -95,7 +95,7 @@ export default { class="d-flex" icon="folder-new" icon-classes="mr-2" - @click="createNewItem($options.modalTypes.tree);" + @click="createNewItem($options.modalTypes.tree)" /> </li> <li class="divider"></li> @@ -106,7 +106,7 @@ export default { class="d-flex" icon="pencil" icon-classes="mr-2" - @click="createNewItem($options.modalTypes.rename);" + @click="createNewItem($options.modalTypes.rename)" /> </li> <li> @@ -115,7 +115,7 @@ export default { class="d-flex" icon="remove" icon-classes="mr-2" - @click="deleteEntry(path);" + @click="deleteEntry(path)" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 63cbf41b89b..04ecd4ba4e7 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -114,7 +114,7 @@ export default { <button type="button" class="btn btn-missing p-1 pr-2 pl-2" - @click="createFromTemplate(template);" + @click="createFromTemplate(template)" > {{ template.name }} </button> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 7a57ccf2dd3..2e6bd85feec 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -122,7 +122,7 @@ export default { data-placement="left" class="ide-sidebar-link is-right" type="button" - @click="clickTab($event, tab);" + @click="clickTab($event, tab)" > <icon :size="16" :name="tab.icon" /> </button> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index c13d3ec094b..94a9e87369c 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -219,7 +219,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'editor' });" + @click.prevent="setFileViewMode({ file, viewMode: 'editor' })" > <template v-if="viewer === $options.viewerTypes.edit"> {{ __('Edit') }} @@ -233,7 +233,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'preview' });" + @click.prevent="setFileViewMode({ file, viewMode: 'preview' })" > {{ file.previewMode.previewTitle }} </a> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 4b87b83db8a..f6aa2295844 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -74,7 +74,7 @@ export default { active: tab.active, disabled: tab.pending, }" - @click="clickFile(tab);" + @click="clickFile(tab)" @mouseover="mouseOverTab" @mouseout="mouseOutTab" > @@ -88,7 +88,7 @@ export default { :disabled="tab.pending" type="button" class="multi-file-tab-close" - @click.stop.prevent="closeFile(tab);" + @click.stop.prevent="closeFile(tab)" > <icon v-if="!showChangedIcon" :size="12" name="close" /> <changed-file-icon v-else :file="tab" /> diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue index a89de56ab5c..7277fcb7617 100644 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -78,8 +78,8 @@ export default { :min-size="minSize" :max-size="$options.maxSize" :side="side === 'right' ? 'left' : 'right'" - @resize-start="setResizingStatus(true);" - @resize-end="setResizingStatus(false);" + @resize-start="setResizingStatus(true)" + @resize-end="setResizingStatus(false)" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue index f58e08c2cc9..de3e71dad92 100644 --- a/app/assets/javascripts/ide/components/shared/tokened_input.vue +++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue @@ -76,8 +76,8 @@ export default { <button class="selectable btn-blank" type="button" - @click.stop="removeToken(token);" - @keyup.delete="removeToken(token);" + @click.stop="removeToken(token)" + @keyup.delete="removeToken(token)" > <div class="value-container rounded"> <div class="value">{{ token.label }}</div> diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 6351948f750..5a2b680c2f7 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -10,13 +10,20 @@ import { parseBoolean } from '../lib/utils/common_utils'; Vue.use(Translate); /** + * Function that receives the default store and returns an extended one. + * @callback extendStoreCallback + * @param {Vuex.Store} store + * @param {Element} el + */ + +/** * Initialize the IDE on the given element. * * @param {Element} el - The element that will contain the IDE. * @param {Object} options - Extra options for the IDE (Used by EE). * @param {Component} options.rootComponent - * Component that overrides the root component. - * @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore - + * @param {extendStoreCallback} options.extendStore - * Function that receives the default store and returns an extended one. */ export function initIde(el, options = {}) { diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index d2b7ce18290..d473d6a482d 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -80,7 +80,6 @@ export default { 'hasError', ]), ...mapGetters([ - 'headerActions', 'headerTime', 'shouldRenderCalloutMessage', 'shouldRenderTriggeredLabel', @@ -202,7 +201,6 @@ export default { :item-id="job.id" :time="headerTime" :user="job.user" - :actions="headerActions" :has-sidebar-button="true" :should-render-triggered-label="shouldRenderTriggeredLabel" :item-name="__('Job')" diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index ad3e7dabc79..a2141dc3760 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -48,8 +48,7 @@ export default { return `${this.job.runner.description} (#${this.job.runner.id})`; }, retryButtonClass() { - let className = - 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; + let className = 'js-retry-button btn btn-retry'; className += this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; return className; @@ -110,24 +109,27 @@ export default { <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix"> <div class="sidebar-container"> <div class="blocks-container"> - <div class="block d-flex align-items-center"> - <h4 class="flex-grow-1 prepend-top-8 m-0">{{ job.name }}</h4> - <gl-link - v-if="job.retry_path" - :class="retryButtonClass" - :href="job.retry_path" - data-method="post" - rel="nofollow" - >{{ __('Retry') }}</gl-link - > - <gl-link - v-if="job.terminal_path" - :href="job.terminal_path" - class="js-terminal-link pull-right btn btn-primary btn-inverted visible-md-block visible-lg-block" - target="_blank" - > - {{ __('Debug') }} <icon name="external-link" /> - </gl-link> + <div class="block d-flex flex-nowrap align-items-center"> + <h4 class="my-0 mr-2">{{ job.name }}</h4> + <div class="flex-grow-1 flex-shrink-0 text-right"> + <gl-link + v-if="job.retry_path" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" + >{{ __('Retry') }}</gl-link + > + <gl-link + v-if="job.cancel_path" + :href="job.cancel_path" + class="js-cancel-job btn btn-default" + data-method="post" + rel="nofollow" + >{{ __('Cancel') }}</gl-link + > + </div> + <gl-button :aria-label="__('Toggle Sidebar')" type="button" @@ -137,22 +139,24 @@ export default { <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> </gl-button> </div> - <div v-if="job.retry_path || job.new_issue_path" class="block retry-link"> + + <div v-if="job.terminal_path || job.new_issue_path" class="block retry-link"> <gl-link v-if="job.new_issue_path" :href="job.new_issue_path" - class="js-new-issue btn btn-success btn-inverted" + class="js-new-issue btn btn-success btn-inverted float-left mr-2" >{{ __('New issue') }}</gl-link > <gl-link - v-if="job.retry_path" - :href="job.retry_path" - class="js-retry-job btn btn-inverted-secondary" - data-method="post" - rel="nofollow" - >{{ __('Retry') }}</gl-link + v-if="job.terminal_path" + :href="job.terminal_path" + class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left" + target="_blank" > + {{ __('Debug') }} <icon name="external-link" :size="14" /> + </gl-link> </div> + <div :class="{ block: renderBlock }"> <detail-row v-if="job.duration" @@ -193,16 +197,6 @@ export default { tag }}</span> </p> - - <div v-if="job.cancel_path" class="btn-group prepend-top-5" role="group"> - <gl-link - :href="job.cancel_path" - class="js-cancel-job btn btn-sm btn-default" - data-method="post" - rel="nofollow" - >{{ __('Cancel') }}</gl-link - > - </div> </div> <artifacts-block v-if="hasArtifact" :artifact="job.artifact" /> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 7f79e92067f..91332c21b52 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -55,7 +55,7 @@ export default { <ul class="dropdown-menu"> <li v-for="stage in stages" :key="stage.name"> - <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage);"> + <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage)"> {{ stage.name }} </button> </li> diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 35e92b0b5d9..98911717381 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -1,22 +1,6 @@ import _ from 'underscore'; -import { __ } from '~/locale'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; -export const headerActions = state => { - if (state.job.new_issue_path) { - return [ - { - label: __('New issue'), - path: state.job.new_issue_path, - cssClass: - 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', - type: 'link', - }, - ]; - } - return []; -}; - export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); export const shouldRenderCalloutMessage = state => diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index b8c3c237eb3..4314e5e1afb 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -11,48 +11,53 @@ function hideEndFade($scrollingTabs) { }); } +function initDeferred() { + $(document).trigger('init.scrolling-tabs'); +} + export default function initLayoutNav() { const contextualSidebar = new ContextualSidebar(); contextualSidebar.bindEvents(); initFlyOutNav(); - $(document) - .on('init.scrolling-tabs', () => { - const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); - $scrollingTabs.addClass('is-initialized'); + // We need to init it on DomContentLoaded as others could also call it + $(document).on('init.scrolling-tabs', () => { + const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); + $scrollingTabs.addClass('is-initialized'); - $(window) - .on('resize.nav', () => { - hideEndFade($scrollingTabs); - }) - .trigger('resize.nav'); + $(window) + .on('resize.nav', () => { + hideEndFade($scrollingTabs); + }) + .trigger('resize.nav'); - $scrollingTabs.on('scroll', function tabsScrollEvent() { - const $this = $(this); - const currentPosition = $this.scrollLeft(); - const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); + $scrollingTabs.on('scroll', function tabsScrollEvent() { + const $this = $(this); + const currentPosition = $this.scrollLeft(); + const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); - $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); - $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); - }); + $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); + $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); + }); - $scrollingTabs.each(function scrollTabsEachLoop() { - const $this = $(this); - const scrollingTabWidth = $this.width(); - const $active = $this.find('.active'); - const activeWidth = $active.width(); + $scrollingTabs.each(function scrollTabsEachLoop() { + const $this = $(this); + const scrollingTabWidth = $this.width(); + const $active = $this.find('.active'); + const activeWidth = $active.width(); - if ($active.length) { - const offset = $active.offset().left + activeWidth; + if ($active.length) { + const offset = $active.offset().left + activeWidth; - if (offset > scrollingTabWidth - 30) { - const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2; + if (offset > scrollingTabWidth - 30) { + const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2; - $this.scrollLeft(scrollLeft); - } + $this.scrollLeft(scrollLeft); } - }); - }) - .trigger('init.scrolling-tabs'); + } + }); + }); + + requestIdleCallback(initDeferred); } diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index ee01a73a6e8..66f25b622e0 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -163,6 +163,7 @@ export default class LazyLoader { img.removeAttribute('data-src'); img.classList.remove('lazy'); img.classList.add('js-lazy-loaded'); + img.classList.add('qa-js-lazy-loaded'); } } } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index fc34d243dd7..3b6a57dad44 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,3 +1,7 @@ +/** + * @module common-utils + */ + import $ from 'jquery'; import axios from './axios_utils'; import { getLocationHash } from './url_utility'; @@ -426,13 +430,14 @@ export const historyPushState = newUrl => { }; /** - * Returns true for a String "true" and false otherwise. - * This is the opposite of Boolean(...).toString() + * Returns true for a String value of "true" and false otherwise. + * This is the opposite of Boolean(...).toString(). + * `parseBoolean` is idempotent. * * @param {String} value * @returns {Boolean} */ -export const parseBoolean = value => value === 'true'; +export const parseBoolean = value => (value && value.toString()) === 'true'; /** * Converts permission provided as strings to booleans. @@ -450,10 +455,16 @@ export const convertPermissionToBoolean = permission => { }; /** + * @callback backOffCallback + * @param {Function} next + * @param {Function} stop + */ + +/** * Back Off exponential algorithm * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> * - * @param {Function<next, stop>} fn function to be called + * @param {backOffCallback} fn function to be called * @param {Number} timeout * @return {Promise<Any, Error>} * @example diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js index 5246c49842e..68b64a3a16a 100644 --- a/app/assets/javascripts/locale/sprintf.js +++ b/app/assets/javascripts/locale/sprintf.js @@ -4,8 +4,8 @@ import _ from 'underscore'; Very limited implementation of sprintf supporting only named parameters. @param input (translated) text with parameters (e.g. '%{num_users} users use us') - @param parameters object mapping parameter names to values (e.g. { num_users: 5 }) - @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) + @param {Object} parameters object mapping parameter names to values (e.g. { num_users: 5 }) + @param {Boolean} escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) @returns {String} the text with parameters replaces (e.g. '5 users use us') @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 64a1df80a8e..309b73f5a4d 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -257,8 +257,8 @@ export default { <template> <div class="prometheus-graph" - @mouseover="showFlagContent = true;" - @mouseleave="showFlagContent = false;" + @mouseover="showFlagContent = true" + @mouseleave="showFlagContent = false" > <div class="prometheus-graph-header"> <h5 class="prometheus-graph-title">{{ graphData.title }}</h5> @@ -300,7 +300,7 @@ export default { :height="graphHeight - 100" class="prometheus-graph-overlay" transform="translate(-5, 20)" - @mousemove="handleMouseOverGraph($event);" + @mousemove="handleMouseOverGraph($event)" /> </svg> <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display"> diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 7c17147dd01..d669ba5a8fa 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -357,9 +357,9 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" aria-label="Description" placeholder="Write a comment or drag your files here…" - @keydown.up="editCurrentUserLastNote();" - @keydown.meta.enter="handleSave();" - @keydown.ctrl.enter="handleSave();" + @keydown.up="editCurrentUserLastNote()" + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()" > </textarea> </markdown-field> @@ -373,7 +373,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button" type="submit" - @click.prevent="handleSave();" + @click.prevent="handleSave()" > {{ __(commentButtonTitle) }} </button> @@ -394,7 +394,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <button type="button" class="btn btn-transparent" - @click.prevent="setNoteType('comment');" + @click.prevent="setNoteType('comment')" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> @@ -408,7 +408,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <button type="button" class="btn btn-transparent qa-discussion-option" - @click.prevent="setNoteType('discussion');" + @click.prevent="setNoteType('discussion')" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> @@ -429,7 +429,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ]" :disabled="isToggleStateButtonLoading || isSubmitting" :label="issueActionButtonTitle" - @click="handleSave(true);" + @click="handleSave(true)" /> </div> </form> diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 2d7c04ea614..e03d6e9cd02 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -112,7 +112,7 @@ export default { :class="{ 'is-active': filter.value === currentValue }" class="qa-filter-options" type="button" - @click="selectFilter(filter.value);" + @click="selectFilter(filter.value)" > {{ filter.title }} </button> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index bde00ea87ff..3efdd1c5c17 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -174,7 +174,7 @@ export default { data-placement="bottom" class="btn award-control" type="button" - @click="handleAward(awardName);" + @click="handleAward(awardName)" > <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter">{{ awardList.length }}</span> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index e55006f2e43..269b4a4b117 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -220,10 +220,10 @@ export default { class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" aria-label="Description" placeholder="Write a comment or drag your files here…" - @keydown.meta.enter="handleKeySubmit();" - @keydown.ctrl.enter="handleKeySubmit();" - @keydown.up="editMyLastNote();" - @keydown.esc="cancelHandler(true);" + @keydown.meta.enter="handleKeySubmit()" + @keydown.ctrl.enter="handleKeySubmit()" + @keydown.up="editMyLastNote()" + @keydown.esc="cancelHandler(true)" ></textarea> </markdown-field> <div class="note-form-actions clearfix"> @@ -231,21 +231,21 @@ export default { :disabled="isDisabled" type="button" class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" - @click="handleUpdate();" + @click="handleUpdate()" > {{ saveButtonTitle }} </button> <button v-if="discussion.resolvable" class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" - @click.prevent="handleUpdate(true);" + @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} </button> <button class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" type="button" - @click="cancelHandler();" + @click="cancelHandler()" > Cancel </button> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 7c3f5d00308..4480ec74182 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -440,7 +440,7 @@ Please check your network connection and try again.`; <button type="button" class="btn btn-default ml-sm-2" - @click="resolveHandler();" + @click="resolveHandler()" > <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i> {{ resolveButtonTitle }} diff --git a/app/assets/javascripts/pages/admin/application_settings/show/index.js b/app/assets/javascripts/pages/admin/application_settings/show/index.js new file mode 100644 index 00000000000..5ec9688a6e4 --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/show/index.js @@ -0,0 +1,3 @@ +import initUserInternalRegexPlaceholder from '../../application_settings/account_and_limits'; + +document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder()); diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js index 3aa793e47b9..8a32556f06c 100644 --- a/app/assets/javascripts/pages/admin/index.js +++ b/app/assets/javascripts/pages/admin/index.js @@ -1,7 +1,3 @@ import initAdmin from './admin'; -import initUserInternalRegexPlaceholder from './application_settings/account_and_limits'; -document.addEventListener('DOMContentLoaded', () => { - initAdmin(); - initUserInternalRegexPlaceholder(); -}); +document.addEventListener('DOMContentLoaded', initAdmin()); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index db2a4041ec0..bd4309e47ad 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -70,7 +70,7 @@ export default { :checked="isEditable" class="label-bold" type="radio" - @click="toggleCustomInput(true);" + @click="toggleCustomInput(true)" /> <label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label> @@ -88,7 +88,7 @@ export default { :value="cronIntervalPresets.everyDay" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-day"> {{ __('Every day (at 4:00am)') }} </label> @@ -102,7 +102,7 @@ export default { :value="cronIntervalPresets.everyWeek" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-week"> @@ -118,7 +118,7 @@ export default { :value="cronIntervalPresets.everyMonth" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-month"> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 59cebaba717..a49dc311bd0 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,59 +1,20 @@ <script> -import _ from 'underscore'; import { GlLoadingIcon } from '@gitlab/ui'; import StageColumnComponent from './stage_column_component.vue'; +import GraphMixin from '../../mixins/graph_component_mixin'; export default { components: { StageColumnComponent, GlLoadingIcon, }, - props: { - isLoading: { - type: Boolean, - required: true, - }, - pipeline: { - type: Object, - required: true, - }, - }, - computed: { - graph() { - return this.pipeline.details && this.pipeline.details.stages; - }, - }, - methods: { - capitalizeStageName(name) { - const escapedName = _.escape(name); - return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); - }, - isFirstColumn(index) { - return index === 0; - }, - stageConnectorClass(index, stage) { - let className; - - // If it's the first stage column and only has one job - if (index === 0 && stage.groups.length === 1) { - className = 'no-margin'; - } else if (index > 0) { - // If it is not the first column - className = 'left-margin'; - } - - return className; - }, - refreshPipelineGraph() { - this.$emit('refreshPipelineGraph'); - }, - }, + mixins: [GraphMixin], }; </script> <template> <div class="build-content middle-block js-pipeline-graph"> <div class="pipeline-visualization pipeline-graph pipeline-tab-content"> - <div class="text-center"><gl-loading-icon v-if="isLoading" :size="3" /></div> + <div v-if="isLoading" class="m-auto"><gl-loading-icon :size="3" /></div> <ul v-if="!isLoading" class="stage-column-list"> <stage-column-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 2e9f2519fcb..0152e2fbe04 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -77,7 +77,7 @@ export default { :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" class="js-pipeline-action-link no-btn btn" - @click="onClickAction(action);" + @click="onClickAction(action)" > {{ action.name }} <span v-if="action.scheduled_at" class="pull-right"> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 2d3f667e73e..7426936515a 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -172,8 +172,6 @@ export default { <span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events"> <icon :name="borderlessIcon" /> </span> - - <i class="fa fa-caret-down" aria-hidden="true"> </i> </button> <div diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js new file mode 100644 index 00000000000..66e9476dadf --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js @@ -0,0 +1,44 @@ +import _ from 'underscore'; + +export default { + props: { + isLoading: { + type: Boolean, + required: true, + }, + pipeline: { + type: Object, + required: true, + }, + }, + computed: { + graph() { + return this.pipeline.details && this.pipeline.details.stages; + }, + }, + methods: { + capitalizeStageName(name) { + const escapedName = _.escape(name); + return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); + }, + isFirstColumn(index) { + return index === 0; + }, + stageConnectorClass(index, stage) { + let className; + + // If it's the first stage column and only has one job + if (index === 0 && stage.groups.length === 1) { + className = 'no-margin'; + } else if (index > 0) { + // If it is not the first column + className = 'left-margin'; + } + + return className; + }, + refreshPipelineGraph() { + this.$emit('refreshPipelineGraph'); + }, + }, +}; diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index 21095fcba16..83811ab489a 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -108,9 +108,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.id"> - <button type="button" @click.prevent="setItem(result.name);"> - {{ result.name }} - </button> + <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue index 056584c8865..a2eb79af4f9 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue @@ -169,7 +169,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.project_number"> - <button type="button" @click.prevent="setItem(result);">{{ result.name }}</button> + <button type="button" @click.prevent="setItem(result)">{{ result.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index 728616a441f..5f8a4946f4a 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -82,9 +82,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.id"> - <button type="button" @click.prevent="setItem(result.name);"> - {{ result.name }} - </button> + <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 2c19973a114..81fe0a48c06 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -106,7 +106,7 @@ export default { :aria-label="s__('ContainerRegistry|Remove tag')" variant="danger" class="js-delete-registry d-none d-sm-block float-right" - @click="handleDeleteRegistry(item);" + @click="handleDeleteRegistry(item)" > <icon name="remove" /> </gl-button> diff --git a/app/assets/javascripts/reports/components/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue index 118e4b02c46..4f81cee2a38 100644 --- a/app/assets/javascripts/reports/components/modal_open_name.vue +++ b/app/assets/javascripts/reports/components/modal_open_name.vue @@ -26,7 +26,7 @@ export default { <button type="button" class="btn-link btn-blank text-left break-link vulnerability-name-button" - @click="handleIssueClick();" + @click="handleIssueClick()" > {{ issue.title }} </button> diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index 938e83de546..7700f49bf7d 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -30,7 +30,7 @@ export default { <button type="button" class="btn-link btn-blank text-left break-link vulnerability-name-button" - @click="openModal({ issue });" + @click="openModal({ issue })" > <div v-if="isNew" class="badge badge-danger append-right-5">{{ s__('New') }}</div> {{ issue.name }} diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index f04f7606976..7f86741ed29 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -219,7 +219,7 @@ export default { name="button" type="button" class="js-clear-user-status-button clear-user-status btn" - @click="clearStatusInputs();" + @click="clearStatusInputs()" > <icon name="close" /> </button> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index d3a4f9c81e0..c03b2a68c78 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -102,13 +102,13 @@ export default { /> <div class="title hide-collapsed"> {{ __('Time tracking') }} - <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true);"> + <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true)"> <i class="fa fa-question-circle" aria-hidden="true"> </i> </div> <div v-if="showHelpState" class="close-help-button float-right" - @click="toggleHelpState(false);" + @click="toggleHelpState(false)" > <i class="fa fa-close" aria-hidden="true"> </i> </div> diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index 560f50ebf8f..e5dd7a465ea 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -2,11 +2,13 @@ import _ from 'underscore'; import $ from 'jquery'; import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; +import * as webLinks from 'xterm/lib/addons/webLinks/webLinks'; import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils'; const SCROLL_MARGIN = 5; Terminal.applyAddon(fit); +Terminal.applyAddon(webLinks); export default class GLTerminal { constructor(element, options = {}) { @@ -48,6 +50,7 @@ export default class GLTerminal { this.terminal.open(this.container); this.terminal.fit(); + this.terminal.webLinksInit(); this.terminal.focus(); this.socket.onopen = () => { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 84c8a3464a5..0cafa73362e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -12,7 +12,7 @@ export default { name: 'ReadyToMerge', components: { statusIcon, - 'squash-before-merge': SquashBeforeMerge, + SquashBeforeMerge, }, props: { mr: { type: Object, required: true }, @@ -28,6 +28,7 @@ export default { isMakingRequest: false, isMergingImmediately: false, commitMessage: this.mr.commitMessage, + squashBeforeMerge: this.mr.squash, successSvg, warningSvg, }; @@ -110,12 +111,6 @@ export default { return enableSquashBeforeMerge && commitsCount > 1; }, }, - created() { - eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash); - }, - beforeDestroy() { - eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash); - }, methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; @@ -143,7 +138,7 @@ export default { commit_message: this.commitMessage, merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, - squash: this.mr.squash, + squash: this.squashBeforeMerge, }; this.isMakingRequest = true; @@ -166,9 +161,6 @@ export default { new Flash('Something went wrong. Please try again.'); // eslint-disable-line }); }, - handleUpdateSquash(val) { - this.mr.squash = val; - }, initiateMergePolling() { simplePoll((continuePolling, stopPolling) => { this.handleMergePolling(continuePolling, stopPolling); @@ -249,7 +241,7 @@ export default { :class="mergeButtonClass" type="button" class="qa-merge-button" - @click="handleMergeButtonClick();" + @click="handleMergeButtonClick()" > <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> {{ mergeButtonText }} @@ -273,7 +265,7 @@ export default { <a class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" href="#" - @click.prevent="handleMergeButtonClick(true);" + @click.prevent="handleMergeButtonClick(true)" > <span class="media"> <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> @@ -285,7 +277,7 @@ export default { <a class="accept-merge-request qa-merge-immediately-option" href="#" - @click.prevent="handleMergeButtonClick(false, true);" + @click.prevent="handleMergeButtonClick(false, true)" > <span class="media"> <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> @@ -311,8 +303,9 @@ export default { <!-- Placeholder for EE extension of this component --> <squash-before-merge v-if="shouldShowSquashBeforeMerge" - :mr="mr" - :is-merge-button-disabled="isMergeButtonDisabled" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isMergeButtonDisabled" /> <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index e71acf0d7dd..b1f5655a15a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -1,6 +1,5 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; -import eventHub from '~/vue_merge_request_widget/event_hub'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -11,23 +10,19 @@ export default { tooltip, }, props: { - mr: { - type: Object, - required: true, - }, - isMergeButtonDisabled: { + value: { type: Boolean, required: true, }, - }, - data() { - return { - squashBeforeMerge: this.mr.squash, - }; - }, - methods: { - updateSquashModel() { - eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge); + helpPath: { + type: String, + required: false, + default: '', + }, + isDisabled: { + type: Boolean, + required: false, + default: false, }, }, }; @@ -37,18 +32,19 @@ export default { <div class="accept-control inline"> <label class="merge-param-checkbox"> <input - v-model="squashBeforeMerge" - :disabled="isMergeButtonDisabled" + :checked="value" + :disabled="isDisabled" type="checkbox" name="squash" class="qa-squash-checkbox" - @change="updateSquashModel" + @change="$emit('input', $event.target.checked)" /> {{ __('Squash commits') }} </label> <a + v-if="helpPath" v-tooltip - :href="mr.squashBeforeMergeHelpPath" + :href="helpPath" data-title="About this feature" data-placement="bottom" target="_blank" diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index b7f12076958..5a9d86594b1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -32,7 +32,6 @@ import MRWidgetStore from './stores/ee_switch_mr_widget_store'; import MRWidgetService from './services/ee_switch_mr_widget_service'; import eventHub from './event_hub'; import stateMaps from './stores/ee_switch_state_maps'; -import SquashBeforeMerge from './components/states/squash_before_merge.vue'; import notify from '~/lib/utils/notify'; import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; @@ -59,7 +58,6 @@ export default { 'mr-widget-missing-branch': MissingBranchState, 'mr-widget-ready-to-merge': ReadyToMergeState, 'sha-mismatch': ShaMismatchState, - 'mr-widget-squash-before-merge': SquashBeforeMerge, 'mr-widget-checking': CheckingState, 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, 'mr-widget-pipeline-blocked': PipelineBlockedState, diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue index 4abf795f7bd..eabf5d4bf60 100644 --- a/app/assets/javascripts/vue_shared/components/bar_chart.vue +++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue @@ -293,8 +293,8 @@ export default { :title="setTooltipTitle(data)" class="bar-rect" data-placement="top" - @mouseover="barHoveredIn(index);" - @mouseout="barHoveredOut(index);" + @mouseover="barHoveredIn(index)" + @mouseout="barHoveredOut(index)" /> </template> </g> diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 2129f90d497..36b3ee05456 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -95,7 +95,7 @@ export default { class="close float-right" data-dismiss="modal" aria-label="Close" - @click="emitCancel($event);" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -112,7 +112,7 @@ export default { type="button" class="btn" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > {{ closeButtonLabel }} </button> @@ -130,7 +130,7 @@ export default { type="button" class="btn js-primary-button" data-dismiss="modal" - @click="emitSubmit($event);" + @click="emitSubmit($event)" > {{ primaryButtonLabel }} </button> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue index d5fda7e4ed3..cab92297ca7 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue @@ -75,7 +75,7 @@ export default { :class="{ active: mode === $options.imageViewMode.twoup, }" - @click="changeMode($options.imageViewMode.twoup);" + @click="changeMode($options.imageViewMode.twoup)" > {{ s__('ImageDiffViewer|2-up') }} </li> @@ -83,7 +83,7 @@ export default { :class="{ active: mode === $options.imageViewMode.swipe, }" - @click="changeMode($options.imageViewMode.swipe);" + @click="changeMode($options.imageViewMode.swipe)" > {{ s__('ImageDiffViewer|Swipe') }} </li> @@ -91,7 +91,7 @@ export default { :class="{ active: mode === $options.imageViewMode.onion, }" - @click="changeMode($options.imageViewMode.onion);" + @click="changeMode($options.imageViewMode.onion)" > {{ s__('ImageDiffViewer|Onion skin') }} </li> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 4c884c55a30..f54033efc54 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -139,8 +139,8 @@ export default { class="file-row" role="button" @click="clickFile" - @mouseover="toggleHover(true);" - @mouseout="toggleHover(false);" + @mouseover="toggleHover(true)" + @mouseout="toggleHover(false)" > <div class="file-row-name-container"> <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index faf4181bbaf..438851e5ac7 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -81,7 +81,7 @@ export default { type="button" class="close js-modal-close-action" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -96,7 +96,7 @@ export default { type="button" class="btn js-modal-cancel-action qa-modal-cancel-button" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > {{ s__('Modal|Cancel') }} </button> @@ -105,7 +105,7 @@ export default { type="button" class="btn js-modal-primary-action qa-modal-primary-button" data-dismiss="modal" - @click="emitSubmit($event);" + @click="emitSubmit($event)" > {{ footerPrimaryButtonText }} </button> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index c830f5b49b6..3f45dc7853b 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -148,7 +148,7 @@ export default { :class="action.cssClass" container-class="d-inline" :label="action.label" - @click="onClickAction(action);" + @click="onClickAction(action)" /> </template> </section> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 937a2847a58..cc07ef46064 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -182,9 +182,9 @@ export default { this.hasSuggestion = data.references.suggestions && data.references.suggestions.length; } - this.$nextTick(() => { - $(this.$refs['markdown-preview']).renderGFM(); - }); + this.$nextTick() + .then(() => $(this.$refs['markdown-preview']).renderGFM()) + .catch(() => new Flash(__('Error rendering markdown preview'))); }, versionedPreviewPath() { diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index bf4d42670ee..dbfa32cd0ce 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -78,12 +78,7 @@ export default { <div class="md-header"> <ul class="nav-links clearfix"> <li :class="{ active: !previewMarkdown }" class="md-header-tab"> - <button - class="js-write-link" - tabindex="-1" - type="button" - @click="writeMarkdownTab($event);" - > + <button class="js-write-link" tabindex="-1" type="button" @click="writeMarkdownTab($event)"> Write </button> </li> @@ -92,7 +87,7 @@ export default { class="js-preview-link js-md-preview-button" tabindex="-1" type="button" - @click="previewMarkdownTab($event);" + @click="previewMarkdownTab($event)" > Preview </button> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index 563e2f94fcc..c5a2aa1f2af 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -42,7 +42,7 @@ export default { <div class="md-suggestion-header border-bottom-0 mt-2"> <div class="qa-suggestion-diff-header font-weight-bold"> {{ __('Suggested change') }} - <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')"> + <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn"> <icon name="question-o" css-classes="link-highlight" /> </a> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 721f0276ac8..dcda701f049 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -129,7 +129,7 @@ export default { <template> <div> - <div class="flash-container mt-3"></div> + <div class="flash-container js-suggestions-flash"></div> <div v-show="isRendered" ref="container" v-html="noteHtml"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 09a64502819..f8983a3d29a 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -58,7 +58,7 @@ export default { active: tab.isActive, }" > - <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab);"> + <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab)"> {{ tab.name }} <span v-if="shouldRenderBadge(tab.count)" class="badge badge-pill"> {{ tab.count }} </span> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 31df26f7b05..b0af8399955 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -97,7 +97,7 @@ export default { v-html="note.note_html" ></div> <div v-if="hasMoreCommits" class="flex-list"> - <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;"> + <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded"> <icon :name="toggleIcon" :size="8" class="append-right-5" /> <span>Toggle commit list</span> </div> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 1c6c3fc4734..df19906309c 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -19,7 +19,7 @@ export default { data() { return { script: {}, - scriptSrc: 'https://www.google.com/recaptcha/api.js', + scriptSrc: 'https://www.recaptcha.net/recaptcha/api.js', }; }, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 82067129c57..6c0c7f15943 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -134,7 +134,7 @@ export default { <button type="button" class="btn-blank btn-link btn-secondary-hover-link" - @click="newDateSelected(null);" + @click="newDateSelected(null)" > remove </button> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 01e655d27e5..2a34b4630f2 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -149,7 +149,7 @@ export default { }" class="page-item" > - <a class="page-link" @click.prevent="changePage(item.title, item.disabled);"> + <a class="page-link" @click.prevent="changePage(item.title, item.disabled)"> {{ item.title }} </a> </li> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue index 7361867edc5..8eaf8386b99 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue @@ -23,6 +23,11 @@ export default { required: false, default: 20, }, + emptyText: { + type: String, + required: false, + default: __('None'), + }, }, data() { return { @@ -65,7 +70,8 @@ export default { </script> <template> - <div> + <div v-if="!items.length">{{ emptyText }}</div> + <div v-else> <user-avatar-link v-for="item in visibleItems" :key="item.id" diff --git a/app/assets/javascripts/vuex_shared/modules/modal/actions.js b/app/assets/javascripts/vuex_shared/modules/modal/actions.js index 552237e05c5..7b209909f69 100644 --- a/app/assets/javascripts/vuex_shared/modules/modal/actions.js +++ b/app/assets/javascripts/vuex_shared/modules/modal/actions.js @@ -15,3 +15,6 @@ export const show = ({ commit }) => { export const hide = ({ commit }) => { commit(types.HIDE); }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 587127bb059..c8357f7751c 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -225,7 +225,7 @@ h3.popover-header { } .info-well { - background: $theme-gray-50; + background: $gray-50; color: $gl-text-color; border: 1px solid $border-color; border-radius: 4px; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 43d4044033f..4fb787887a1 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -204,7 +204,7 @@ a { [class^="skeleton-line-"] { position: relative; - background-color: $theme-gray-100; + background-color: $gray-100; height: 10px; overflow: hidden; @@ -220,10 +220,10 @@ a { background-size: cover; background-image: linear-gradient( to right, - $theme-gray-100 0%, - $theme-gray-50 20%, - $theme-gray-100 40%, - $theme-gray-100 100% + $gray-100 0%, + $gray-50 20%, + $gray-100 40%, + $gray-100 100% ); height: 10px; } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 7a95db5976d..ad650d45314 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -176,7 +176,7 @@ &.user-authored { cursor: default; background-color: $gray-light; - border-color: $theme-gray-200; + border-color: $gray-200; color: $gl-text-color-disabled; gl-emoji { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a4a9276c580..5d2cbdde8dc 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -483,7 +483,7 @@ // All disabled buttons, regardless of color, type, etc %disabled { background-color: $gray-light !important; - border-color: $theme-gray-200 !important; + border-color: $gray-200 !important; color: $gl-text-color-disabled !important; opacity: 1 !important; cursor: default !important; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cb01a41cb7e..b90db135b4a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -129,7 +129,7 @@ @extend .dropdown-toggle; padding-right: 25px; position: relative; - width: 163px; + width: 160px; text-overflow: ellipsis; overflow: hidden; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 0a0ef2071e9..cbf9ee24ec5 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -261,7 +261,7 @@ label { right: 0.8em; top: 50%; transform: translateY(-50%); - color: $theme-gray-600; + color: $gray-600; } } diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 0ef50e139f2..418eafa153c 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -282,31 +282,31 @@ body { &.ui-dark { @include gitlab-theme( - $theme-gray-200, - $theme-gray-500, - $theme-gray-700, - $theme-gray-800, - $theme-gray-900, + $gray-200, + $gray-500, + $gray-700, + $gray-800, + $gray-900, $white-light ); } &.ui-light { @include gitlab-theme( - $theme-gray-700, - $theme-gray-800, - $theme-gray-700, - $theme-gray-700, - $theme-gray-100, - $theme-gray-700 + $gray-700, + $gray-800, + $gray-700, + $gray-700, + $gray-100, + $gray-700 ); .navbar-gitlab { - background-color: $theme-gray-100; + background-color: $gray-100; box-shadow: 0 1px 0 0 $border-color; .logo-text svg { - fill: $theme-gray-900; + fill: $gray-900; } .navbar-sub-nav, @@ -315,7 +315,7 @@ body { > a:hover, > a:focus, > button:hover { - color: $theme-gray-900; + color: $gray-900; } &.active > a, @@ -329,8 +329,8 @@ body { .container-fluid { .navbar-toggler, .navbar-toggler:hover { - color: $theme-gray-700; - border-left: 1px solid $theme-gray-200; + color: $gray-700; + border-left: 1px solid $gray-200; } } } @@ -348,7 +348,7 @@ body { .search-input-wrap { .search-icon { - fill: $theme-gray-200; + fill: $gray-200; } .search-input { @@ -359,16 +359,16 @@ body { .nav-sidebar li.active { > a { - color: $theme-gray-900; + color: $gray-900; } svg { - fill: $theme-gray-900; + fill: $gray-900; } } .sidebar-top-level-items > li.active .badge.badge-pill { - color: $theme-gray-900; + color: $gray-900; } } } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 8db7d63266e..49b9b7014ae 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -87,8 +87,8 @@ display: flex; align-items: center; justify-content: center; - border: $border-size solid $theme-gray-400; + border: $border-size solid $gray-400; border-radius: 50%; padding: $gl-padding-8 - $border-size; - color: $theme-gray-700; + color: $gray-700; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index ce46d760d7b..679148ddf7b 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -158,7 +158,7 @@ max-height: calc(100vh - 100px); } - table { + table:not(.js-syntax-highlight) { @include markdown-table; } } diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss index 29a2d5881f7..2255d3be75b 100644 --- a/app/assets/stylesheets/framework/stacked_progress_bar.scss +++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss @@ -3,7 +3,7 @@ height: 16px; border-radius: 10px; overflow: hidden; - background-color: $theme-gray-100; + background-color: $gray-100; .status-unavailable, .status-green, @@ -24,7 +24,7 @@ .status-unavailable { padding: 0 10px; - color: $theme-gray-700; + color: $gray-700; } .status-green { @@ -36,11 +36,11 @@ } .status-neutral { - background-color: $theme-gray-200; + background-color: $gray-200; color: $gl-gray-dark; &:hover { - background-color: $theme-gray-300; + background-color: $gray-300; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 0c81dc2e156..45dab036d35 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -143,7 +143,7 @@ margin: 0 0 16px; } - table { + table:not(.js-syntax-highlight) { @extend .table; @extend .table-bordered; margin: 16px 0; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 242977e8543..c1666c728f3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -97,6 +97,18 @@ $red-800: #8b2615; $red-900: #711e11; $red-950: #4b140b; +$gray-50: #fafafa; +$gray-100: #f2f2f2; +$gray-200: #dfdfdf; +$gray-300: #cccccc; +$gray-400: #bababa; +$gray-500: #a7a7a7; +$gray-600: #919191; +$gray-700: #707070; +$gray-800: #4f4f4f; +$gray-900: #2e2e2e; +$gray-950: #1f1f1f; + // GitLab themes $indigo-50: #f7f7ff; @@ -111,18 +123,6 @@ $indigo-800: #393982; $indigo-900: #292961; $indigo-950: #1a1a40; -$theme-gray-50: #fafafa; -$theme-gray-100: #f2f2f2; -$theme-gray-200: #dfdfdf; -$theme-gray-300: #cccccc; -$theme-gray-400: #bababa; -$theme-gray-500: #a7a7a7; -$theme-gray-600: #919191; -$theme-gray-700: #707070; -$theme-gray-800: #4f4f4f; -$theme-gray-900: #2e2e2e; -$theme-gray-950: #1f1f1f; - $theme-blue-50: #f4f8fc; $theme-blue-100: #e6edf5; $theme-blue-200: #c8d7e6; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index 069f45bff49..1dfe2a69a2f 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -5,7 +5,7 @@ $secondary: $gray-light; $input-disabled-bg: $gray-light; -$input-border-color: $theme-gray-200; +$input-border-color: $gray-200; $input-color: $gl-text-color; $font-family-sans-serif: $regular-font; $font-family-monospace: $monospace-font; @@ -20,7 +20,7 @@ $warning: $orange-500; $danger: $red-500; $zindex-modal-backdrop: 1040; $nav-divider-margin-y: ($grid-size / 2); -$dropdown-divider-bg: $theme-gray-200; +$dropdown-divider-bg: $gray-200; $dropdown-item-padding-y: 8px; $dropdown-item-padding-x: 12px; $popover-max-width: 300px; @@ -34,3 +34,12 @@ $h3-font-size: 14px * 1.75; $h4-font-size: 14px * 1.5; $h5-font-size: 14px * 1.25; $h6-font-size: 14px; +$spacer: $grid-size; +$spacers: ( + 0: 0, + 1: ($spacer * .5), + 2: ($spacer), + 3: ($spacer * 2), + 4: ($spacer * 3), + 5: ($spacer * 4) +); diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 98d0a2d43ea..553cc44fe83 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -130,7 +130,7 @@ $ide-commit-header-height: 48px; background: none; border: 0; border-radius: $border-radius-default; - color: $theme-gray-900; + color: $gray-900; svg { position: relative; @@ -145,7 +145,7 @@ $ide-commit-header-height: 48px; } &:not([disabled]):hover { - background-color: $theme-gray-200; + background-color: $gray-200; } &:not([disabled]):focus { @@ -265,7 +265,7 @@ $ide-commit-header-height: 48px; .margin { background-color: $white-light; - border-right: 1px solid $theme-gray-100; + border-right: 1px solid $gray-100; .line-insert { border-right: 1px solid $line-added-dark; @@ -292,7 +292,7 @@ $ide-commit-header-height: 48px; .monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input { - background-color: $theme-gray-50; + background-color: $gray-50; } } } @@ -527,7 +527,7 @@ $ide-commit-header-height: 48px; display: block; margin-left: auto; margin-right: auto; - color: $theme-gray-700; + color: $gray-700; } .file-status-icon { @@ -551,7 +551,7 @@ $ide-commit-header-height: 48px; &:hover, &:focus { - background: $theme-gray-100; + background: $gray-100; outline: 0; @@ -563,7 +563,7 @@ $ide-commit-header-height: 48px; } &:active { - background: $theme-gray-200; + background: $gray-200; } &.is-active { @@ -767,12 +767,12 @@ $ide-commit-header-height: 48px; &:hover { color: $gl-text-color; - background-color: $theme-gray-100; + background-color: $gray-100; } &:focus { color: $gl-text-color; - background-color: $theme-gray-200; + background-color: $gray-200; } &.active { @@ -1273,10 +1273,10 @@ $ide-commit-header-height: 48px; .ide-entry-dropdown-toggle { padding: $gl-padding-4; color: $gl-text-color; - background-color: $theme-gray-100; + background-color: $gray-100; &:hover { - background-color: $theme-gray-200; + background-color: $gray-200; } &:active, @@ -1331,7 +1331,7 @@ $ide-commit-header-height: 48px; &:focus { outline: 0; box-shadow: none; - border-color: $theme-gray-200; + border-color: $gray-200; } } @@ -1358,7 +1358,7 @@ $ide-commit-header-height: 48px; .ide-commit-editor-header { height: 65px; padding: 8px 16px; - background-color: $theme-gray-50; + background-color: $gray-50; box-shadow: inset 0 -1px $white-dark; } @@ -1370,7 +1370,7 @@ $ide-commit-header-height: 48px; .ide-file-icon-holder { display: flex; align-items: center; - color: $theme-gray-700; + color: $gray-700; } .file-row:hover, diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index c5a0eaaf704..bc28ffb3a92 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -26,6 +26,12 @@ opacity: 0.3; } +.dropdown-projects { + .dropdown-content { + max-height: 200px; + } +} + .dropdown-menu-issues-board-new { width: 320px; @@ -167,6 +173,7 @@ background: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; + flex: 1; } .board-header { @@ -228,9 +235,11 @@ } .board-blank-state { - height: calc(100% - 49px); padding: $gl-padding; background-color: $white-light; + flex: 1; + overflow-y: auto; + overflow-x: hidden; } .board-blank-state-list { @@ -252,9 +261,9 @@ } .board-list-component { - height: calc(100% - 49px); - overflow: hidden; position: relative; + flex: 1; + min-height: 0; // firefox fix } .board-list { @@ -277,7 +286,7 @@ padding: $gl-padding; background: $white-light; border-radius: $border-radius-default; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; line-height: $gl-padding; @@ -669,7 +678,7 @@ } .board-card-info-icon { - color: $theme-gray-600; + color: $gray-600; margin-right: $gl-padding-4; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 1352a004206..65f46e3852a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -261,7 +261,7 @@ .trigger-variables-table-cell { font-size: $gl-font-size-small; line-height: $gl-line-height; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; padding: $gl-padding-4 6px; width: 50%; vertical-align: top; @@ -272,7 +272,13 @@ } .retry-link { - display: none; + display: block; + + .btn { + i { + margin-left: 5px; + } + } .btn-inverted-secondary { color: $blue-500; @@ -281,16 +287,6 @@ color: $white-light; } } - - @include media-breakpoint-down(sm) { - display: block; - - .btn { - i { - margin-left: 5px; - } - } - } } .stage-item { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 75166ffcada..b6abb792709 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -267,7 +267,7 @@ .prometheus-graph-cursor { position: absolute; - background: $theme-gray-600; + background: $gray-600; width: 1px; } @@ -309,7 +309,7 @@ > .arrow::after { border-top: 6px solid transparent; border-bottom: 6px solid transparent; - border-left: 4px solid $theme-gray-50; + border-left: 4px solid $gray-50; } .arrow-shadow { @@ -331,7 +331,7 @@ > .arrow::after { border-top: 6px solid transparent; border-bottom: 6px solid transparent; - border-right: 4px solid $theme-gray-50; + border-right: 4px solid $gray-50; } .arrow-shadow { @@ -364,7 +364,7 @@ } > .popover-title { - background-color: $theme-gray-50; + background-color: $gray-50; border-radius: $border-radius-default $border-radius-default 0 0; } } @@ -439,7 +439,7 @@ } > text { - fill: $theme-gray-600; + fill: $gray-600; font-size: 10px; } } @@ -482,5 +482,5 @@ } .prometheus-table-row-highlight { - background-color: $theme-gray-100; + background-color: $gray-100; } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 4fb1a956fab..83b1680512d 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -44,7 +44,7 @@ } .x-axis-text { - fill: $theme-gray-900; + fill: $gray-900; } .bar-rect { @@ -64,7 +64,7 @@ text { font-weight: bold; font-size: 12px; - fill: $theme-gray-800; + fill: $gray-800; } } } @@ -87,5 +87,5 @@ .animate-flicker { animation: flickerAnimation 1.5s infinite; - fill: $theme-gray-500; + fill: $gray-500; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index f0cb81e0bc3..ebbb5beed81 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -78,7 +78,7 @@ &:hover { background-color: $gray-darker; - color: $theme-gray-900; + color: $gray-900; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index a1069aa9783..e0bdc1341b1 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -934,7 +934,7 @@ .sidebar-collapsed-divider { line-height: 5px; font-size: 12px; - color: $theme-gray-700; + color: $gray-700; + .sidebar-collapsed-icon { padding-top: 0; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d2b9470be69..2372640277e 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -82,7 +82,7 @@ justify-content: space-between; padding: $gl-padding; border-radius: $border-radius-default; - border: 1px solid $theme-gray-100; + border: 1px solid $gray-100; &:last-child { margin-bottom: 0; @@ -257,7 +257,7 @@ } .label-badge { - color: $theme-gray-900; + color: $gray-900; font-weight: $gl-font-weight-normal; padding: $gl-padding-4 $gl-padding-8; border-radius: $border-radius-default; @@ -269,7 +269,7 @@ } .label-badge-gray { - background-color: $theme-gray-100; + background-color: $gray-100; } .label-links { @@ -326,11 +326,11 @@ } .label-action { - color: $theme-gray-800; + color: $gray-800; cursor: pointer; svg { - fill: $theme-gray-800; + fill: $gray-800; } &:hover { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 221b4e934ff..1e4b8d8b7e4 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -64,7 +64,7 @@ &::before { content: ''; - border-left: 1px solid $theme-gray-200; + border-left: 1px solid $gray-200; position: absolute; left: 32px; top: -17px; @@ -907,7 +907,7 @@ } .btn svg { - fill: $theme-gray-700; + fill: $gray-700; } .dropdown-menu { @@ -951,7 +951,7 @@ .coverage { font-size: 12px; - color: $theme-gray-700; + color: $gray-700; line-height: initial; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 94bf32945fc..15f3a2ef4a8 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -8,7 +8,7 @@ $status-box-line-height: 26px; padding: $gl-padding-8; margin-top: $gl-padding-8; border-radius: $border-radius-default; - background-color: $theme-gray-100; + background-color: $gray-100; .milestone { border: 0; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 86f571dd90d..51f755c67af 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -147,7 +147,7 @@ } .sidebar-item-value & { - fill: $theme-gray-700; + fill: $gray-700; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 69b7b80dbf4..23b9e4f9416 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -5,7 +5,7 @@ $note-form-margin-left: 72px; @mixin vertical-line($left) { &::before { content: ''; - border-left: 2px solid $theme-gray-100; + border-left: 2px solid $gray-100; position: absolute; top: 0; bottom: 0; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7a47e0a2836..a28921592ec 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -256,14 +256,25 @@ } } - .mini-pipeline-graph-dropdown-toggle svg { - height: $ci-action-icon-size; - width: $ci-action-icon-size; - position: absolute; - top: -1px; - left: -1px; - z-index: 2; - overflow: visible; + .mini-pipeline-graph-dropdown-toggle { + svg { + height: $ci-action-icon-size; + width: $ci-action-icon-size; + position: absolute; + top: -1px; + left: -1px; + z-index: 2; + overflow: visible; + } + + &:hover, + &:active, + &:focus { + svg { + top: -2px; + left: -2px; + } + } } .stage-container { @@ -293,7 +304,7 @@ width: 7px; position: absolute; right: -7px; - top: 10px; + top: 11px; border-bottom: 2px solid $border-color; } } @@ -433,6 +444,7 @@ } .pipeline-tab-content { + display: flex; width: 100%; background-color: $gray-light; padding: $gl-padding; @@ -707,21 +719,43 @@ font-weight: $gl-font-weight-normal; } -@mixin mini-pipeline-graph-color($color-light, $color-main, $color-dark) { - border-color: $color-main; - color: $color-main; +@mixin mini-pipeline-graph-color( + $color-background-default, + $color-background-hover-focus, + $color-background-active, + $color-foreground-default, + $color-foreground-hover-focus, + $color-foreground-active +) { + background-color: $color-background-default; + border-color: $color-foreground-default; + + svg { + fill: $color-foreground-default; + } &:hover, - &:focus, + &:focus { + background-color: $color-background-hover-focus; + border-color: $color-foreground-hover-focus; + + svg { + fill: $color-foreground-hover-focus; + } + } + &:active { - background-color: $color-light; - border-color: $color-dark; - color: $color-dark; + background-color: $color-background-active; + border-color: $color-foreground-active; svg { - fill: $color-dark; + fill: $color-foreground-active; } } + + &:focus { + box-shadow: 0 0 4px 1px $blue-300; + } } @mixin mini-pipeline-item() { @@ -733,26 +767,32 @@ height: $ci-action-icon-size; margin: 0; padding: 0; - transition: all 0.2s linear; position: relative; vertical-align: middle; + &:hover, + &:active, + &:focus { + outline: none; + border-width: 2px; + } + // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - @include mini-pipeline-graph-color($green-100, $green-500, $green-600); + @include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700); } &.ci-status-icon-failed { - @include mini-pipeline-graph-color($red-100, $red-500, $red-600); + @include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700); } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600); + @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700); } &.ci-status-icon-running { - @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600); + @include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700); } &.ci-status-icon-canceled, @@ -760,42 +800,18 @@ &.ci-status-icon-disabled, &.ci-status-icon-not-found, &.ci-status-icon-manual { - @include mini-pipeline-graph-color(rgba($gl-text-color, 0.1), $gl-text-color, $gl-text-color); + @include mini-pipeline-graph-color($white, $gray-700, $gray-800, $gray-900, $gray-950, $black); } &.ci-status-icon-created, &.ci-status-icon-skipped { - @include mini-pipeline-graph-color(rgba($gray-darkest, 0.1), $gray-darkest, $gray-darkest); + @include mini-pipeline-graph-color($white, $gray-200, $gray-300, $gray-500, $gray-600, $gray-700); } } // Dropdown button in mini pipeline graph button.mini-pipeline-graph-dropdown-toggle { @include mini-pipeline-item(); - - > .fa.fa-caret-down { - position: absolute; - left: 20px; - top: 5px; - display: inline-block; - visibility: hidden; - opacity: 0; - color: inherit; - font-size: 12px; - transition: visibility 0.1s, opacity 0.1s linear; - } - - &:active, - &:focus, - &:hover { - outline: none; - width: 35px; - - .fa.fa-caret-down { - visibility: visible; - opacity: 1; - } - } } /** diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index a353f301d07..45e62913f37 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -60,11 +60,11 @@ } &.ui-dark { - background-color: $theme-gray-900; + background-color: $gray-900; } &.ui-light { - background-color: $theme-gray-200; + background-color: $gray-200; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 2d28333689f..505f6e036e3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -459,7 +459,7 @@ margin-right: $gl-padding-4; margin-bottom: $gl-padding-4; color: $gl-text-color-secondary; - background-color: $theme-gray-100; + background-color: $gray-100; line-height: $gl-btn-line-height; &:hover { @@ -914,7 +914,7 @@ } .repository-language-bar-tooltip-share { - color: $theme-gray-400; + color: $gray-400; } pre.light-well { @@ -1025,8 +1025,10 @@ pre.light-well { margin: 0; } - @include media-breakpoint-up(md) { - .description { + .description { + line-height: 1.5; + + @include media-breakpoint-up(md) { color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index ecd51aa06a4..f7619ccbd20 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -96,7 +96,7 @@ } &.neutral svg { - color: $theme-gray-700; + color: $gray-700; } .ci-status-icon { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index ccfa4e00a5b..c5b9d1f6885 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -297,7 +297,7 @@ .btn-clipboard { background-color: $white-light; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; } .deploy-token-help-block { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a8fc848c879..26cd5dc801f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -177,11 +177,17 @@ class ApplicationController < ActionController::Base # hide existence of the resource, rather tell them they cannot access it using # the provided message status ||= message.present? ? :forbidden : :not_found + template = + if status == :not_found + "errors/not_found" + else + "errors/access_denied" + end respond_to do |format| format.any { head status } format.html do - render "errors/access_denied", + render template, layout: "errors", status: status, locals: { message: message } diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index f073b6de444..b1d224d026f 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -53,6 +53,9 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def load_projects(finder_params) + @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute + @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + projects = ProjectsFinder .new(params: finder_params, current_user: current_user) .execute diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 778fdda8dbd..9f074690cbc 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -55,6 +55,9 @@ class Explore::ProjectsController < Explore::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def load_projects + @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute + @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + projects = ProjectsFinder.new(current_user: current_user, params: params) .execute .includes(:route, :creator, :group, namespace: [:route, :owner]) diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb index d549f793ad7..236a19a8dc4 100644 --- a/app/controllers/groups/children_controller.rb +++ b/app/controllers/groups/children_controller.rb @@ -35,7 +35,7 @@ module Groups def setup_children(parent) @children = GroupDescendantsFinder.new(current_user: current_user, parent_group: parent, - params: params).execute + params: params.to_unsafe_h).execute @children = @children.page(params[:page]) end end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 042b6b1264f..9b45be6db99 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -18,6 +18,7 @@ class Import::BaseController < ApplicationController end # rubocop: enable CodeReuse/ActiveRecord + # deprecated: being replaced by app/services/import/base_service.rb def find_or_create_namespace(names, owner) names = params[:target_namespace].presence || names @@ -32,6 +33,7 @@ class Import::BaseController < ApplicationController current_user.namespace end + # deprecated: being replaced by app/services/import/base_service.rb def project_save_error(project) project.errors.full_messages.join(', ') end diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 575c40d5f6f..87338488eba 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -40,7 +40,7 @@ class Import::BitbucketServerController < Import::BaseController else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity end - rescue BitbucketServer::Client::ServerError => e + rescue BitbucketServer::Connection::ConnectionError => e render json: { errors: "Unable to connect to server: #{e}" }, status: :unprocessable_entity end @@ -62,7 +62,7 @@ class Import::BitbucketServerController < Import::BaseController already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) } - rescue BitbucketServer::Connection::ConnectionError, BitbucketServer::Client::ServerError => e + rescue BitbucketServer::Connection::ConnectionError => e flash[:alert] = "Unable to connect to server: #{e}" clear_session_data redirect_to new_import_bitbucket_server_path diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index d4c26fa0709..34c7dbdc2fe 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -39,28 +39,25 @@ class Import::GithubController < Import::BaseController end def create - repo = client.repo(params[:repo_id].to_i) - project_name = params[:new_name].presence || repo.name - namespace_path = params[:target_namespace].presence || current_user.namespace_path - target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - - if can?(current_user, :create_projects, target_namespace) - project = Gitlab::LegacyGithubImport::ProjectCreator - .new(repo, project_name, target_namespace, current_user, access_params, type: provider) - .execute(extra_project_attrs) - - if project.persisted? - render json: ProjectSerializer.new.represent(project) - else - render json: { errors: project_save_error(project) }, status: :unprocessable_entity - end + result = Import::GithubService.new(client, current_user, import_params).execute(access_params, provider) + + if result[:status] == :success + render json: ProjectSerializer.new.represent(result[:project]) else - render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity + render json: { errors: result[:message] }, status: result[:http_status] end end private + def import_params + params.permit(permitted_import_params) + end + + def permitted_import_params + [:repo_id, :new_name, :target_namespace] + end + def client @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) end @@ -124,10 +121,6 @@ class Import::GithubController < Import::BaseController {} end - def extra_project_attrs - {} - end - def extra_import_params {} end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index c24bf211760..09a384e89ab 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -21,11 +21,22 @@ class Projects::BadgesController < Projects::ApplicationController private + def badge_layout + case params[:style] + when 'flat' + 'badge' + when 'flat-square' + 'badge_flat-square' + else + 'badge' + end + end + def render_badge(badge) respond_to do |format| format.html { render_404 } format.svg do - render 'badge', locals: { badge: badge.template } + render badge_layout, locals: { badge: badge.template } end end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index ff286c0ccf0..77672e7d9fc 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -93,7 +93,7 @@ class Projects::BlobController < Projects::ApplicationController @blob.load_all_data! @lines = @blob.present.highlight.lines - @form = UnfoldForm.new(params) + @form = UnfoldForm.new(params.to_unsafe_h) @lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe) diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb index 4596b6c91f2..9e403e1d25b 100644 --- a/app/controllers/projects/error_tracking_controller.rb +++ b/app/controllers/projects/error_tracking_controller.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class Projects::ErrorTrackingController < Projects::ApplicationController - before_action :check_feature_flag! before_action :authorize_read_sentry_issue! - before_action :push_feature_flag_to_frontend POLLING_INTERVAL = 10_000 @@ -43,12 +41,4 @@ class Projects::ErrorTrackingController < Projects::ApplicationController .new(project: project, user: current_user) .represent(errors) end - - def check_feature_flag! - render_404 unless Feature.enabled?(:error_tracking, project) - end - - def push_feature_flag_to_frontend - push_frontend_feature_flag(:error_tracking, current_user) - end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 21688e54481..e3e60665506 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -178,8 +178,6 @@ class Projects::IssuesController < Projects::ApplicationController end def import_csv - return render_404 unless Feature.enabled?(:issues_import_csv) - if uploader = UploadService.new(project, params[:file]).execute ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id) diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index bfbbcba883f..d5ce790e2d9 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -4,10 +4,10 @@ class Projects::JobsController < Projects::ApplicationController include SendFileUpload include ContinueParams - before_action :build, except: [:index, :cancel_all] + before_action :build, except: [:index] before_action :authorize_read_build! before_action :authorize_update_build!, - except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase] + except: [:index, :show, :status, :raw, :trace, :erase] before_action :authorize_erase_build!, only: [:erase] before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize] before_action :verify_api_request!, only: :terminal_websocket_authorize @@ -39,16 +39,6 @@ class Projects::JobsController < Projects::ApplicationController end # rubocop: enable CodeReuse/ActiveRecord - def cancel_all - return access_denied! unless can?(current_user, :update_build, project) - - @project.builds.running_or_pending.each do |build| - build.cancel if can?(current_user, :update_build, build) - end - - redirect_to project_jobs_path(project) - end - # rubocop: disable CodeReuse/ActiveRecord def show @pipeline = @build.pipeline diff --git a/app/controllers/projects/lfs_locks_api_controller.rb b/app/controllers/projects/lfs_locks_api_controller.rb index fc67cd72faa..6aacb9d9a56 100644 --- a/app/controllers/projects/lfs_locks_api_controller.rb +++ b/app/controllers/projects/lfs_locks_api_controller.rb @@ -4,19 +4,19 @@ class Projects::LfsLocksApiController < Projects::GitHttpClientController include LfsRequest def create - @result = Lfs::LockFileService.new(project, user, params).execute + @result = Lfs::LockFileService.new(project, user, lfs_params).execute render_json(@result[:lock]) end def unlock - @result = Lfs::UnlockFileService.new(project, user, params).execute + @result = Lfs::UnlockFileService.new(project, user, lfs_params).execute render_json(@result[:lock]) end def index - @result = Lfs::LocksFinderService.new(project, user, params).execute + @result = Lfs::LocksFinderService.new(project, user, lfs_params).execute render_json(@result[:locks]) end @@ -69,4 +69,8 @@ class Projects::LfsLocksApiController < Projects::GitHttpClientController def upload_request? %w(create unlock verify).include?(params[:action]) end + + def lfs_params + params.permit(:id, :path, :force) + end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 8e68014a30d..8bc59d8a305 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -144,7 +144,7 @@ class Projects::MilestonesController < Projects::ApplicationController def search_params if request.format.json? && project_group && can?(current_user, :read_group, project_group) - groups = project_group.self_and_ancestors_ids + groups = project_group.self_and_ancestors.select(:id) end params.permit(:state).merge(project_ids: @project.id, group_ids: groups) diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 192e6d38f36..7f988110977 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -4,6 +4,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController before_action :authorize_admin_pipeline! def show - redirect_to project_settings_ci_cd_path(@project, params: params) + redirect_to project_settings_ci_cd_path(@project, params: params.to_unsafe_h) end end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 9c477978f60..fcd54b6106e 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -3,8 +3,8 @@ # Search for milestones # # params - Hash -# project_ids: Array of project ids or single project id. -# group_ids: Array of group ids or single group id. +# project_ids: Array of project ids or single project id or ActiveRecord relation. +# group_ids: Array of group ids or single group id or ActiveRecord relation. # order - Orders by field default due date asc. # title - filter by title. # state - filters by state. @@ -12,17 +12,13 @@ class MilestonesFinder include FinderMethods - attr_reader :params, :project_ids, :group_ids + attr_reader :params def initialize(params = {}) - @project_ids = Array(params[:project_ids]) - @group_ids = Array(params[:group_ids]) @params = params end def execute - return Milestone.none if project_ids.empty? && group_ids.empty? - items = Milestone.all items = by_groups_and_projects(items) items = by_title(items) @@ -34,7 +30,7 @@ class MilestonesFinder private def by_groups_and_projects(items) - items.for_projects_and_groups(project_ids, group_ids) + items.for_projects_and_groups(params[:project_ids], params[:group_ids]) end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 7b22bc8f98f..b3935ae350d 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -7,6 +7,15 @@ module EnvironmentsHelper } end + def environments_folder_list_view_data + { + "endpoint" => folder_project_environments_path(@project, @folder, format: :json), + "folder-name" => @folder, + "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, + "can-read-environment" => can?(current_user, :read_environment, @project).to_s + } + end + def metrics_data(project, environment) { "settings-path" => edit_project_service_path(project, 'prometheus'), diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 5a21403bc5e..ab4a1ccc0d1 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -32,7 +32,7 @@ module MembersHelper end def filter_group_project_member_path(options = {}) - options = params.slice(:search, :sort).merge(options) + options = params.slice(:search, :sort).merge(options).permit! "#{request.path}?#{options.to_param}" end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ebbed08f78a..eceee054ede 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -285,7 +285,7 @@ module ProjectsHelper # overridden in EE def settings_operations_available? - Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project) + can?(current_user, :read_environment, @project) end private diff --git a/app/helpers/release_blog_post_helper.rb b/app/helpers/release_blog_post_helper.rb new file mode 100644 index 00000000000..31b5b7edc39 --- /dev/null +++ b/app/helpers/release_blog_post_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ReleaseBlogPostHelper + def blog_post_url + Gitlab::ReleaseBlogPost.instance.blog_post_url + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index dc6f8ae1a7f..cfdb3c0d719 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -224,8 +224,15 @@ module Ci before_transition any => [:failed] do |build| next unless build.project + next unless build.deployment - build.deployment&.drop + begin + build.deployment.drop! + rescue => e + Gitlab::Sentry.track_exception(e, extra: { build_id: build.id }) + end + + true end after_transition any => [:failed] do |build| diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 7799f069742..7c15aaa4825 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Ingress < ActiveRecord::Base - VERSION = '0.23.0'.freeze + VERSION = '1.1.2'.freeze self.table_name = 'clusters_applications_ingress' diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index e25be522d68..26bf73f4dd8 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -5,7 +5,8 @@ module Clusters class Prometheus < ActiveRecord::Base include PrometheusAdapter - VERSION = '6.7.3'.freeze + VERSION = '6.7.3' + READY_STATUS = [:installed, :updating, :updated, :update_errored].freeze self.table_name = 'clusters_applications_prometheus' @@ -24,12 +25,8 @@ module Clusters end end - def ready_status - [:installed] - end - def ready? - ready_status.include?(status_name) + READY_STATUS.include?(status_name) end def chart @@ -55,6 +52,24 @@ module Clusters ) end + def upgrade_command(values) + ::Gitlab::Kubernetes::Helm::UpgradeCommand.new( + name, + version: VERSION, + chart: chart, + rbac: cluster.platform_kubernetes_rbac?, + files: files_with_replaced_values(values) + ) + end + + # Returns a copy of files where the values of 'values.yaml' + # are replaced by the argument. + # + # See #values for the data format required + def files_with_replaced_values(replaced_values) + files.merge('values.yaml': replaced_values) + end + def prometheus_client return unless kube_client diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 0e74cce29b7..a556dd5ad8b 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -77,6 +77,10 @@ module Clusters def available? installed? || updated? end + + def update_in_progress? + updating? + end end end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index a8c9e54f00c..73a27326f6c 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -15,7 +15,7 @@ module CacheMarkdownField # Increment this number every time the renderer changes its output CACHE_REDCARPET_VERSION = 3 CACHE_COMMONMARK_VERSION_START = 10 - CACHE_COMMONMARK_VERSION = 12 + CACHE_COMMONMARK_VERSION = 13 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 4f73beaafc5..68b2353556e 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -3,6 +3,8 @@ class ExternalIssue include Referable + attr_reader :project + def initialize(issue_identifier, project) @issue_identifier, @project = issue_identifier, project end @@ -32,12 +34,8 @@ class ExternalIssue [self.class, to_s].hash end - def project - @project - end - def project_id - @project.id + project.id end def to_reference(_from = nil, full: nil) diff --git a/app/models/group.rb b/app/models/group.rb index edac2444c4d..22814e35ca8 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -382,6 +382,10 @@ class Group < Namespace end end + def highest_group_member(user) + GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last + end + def hashed_storage?(_feature) false end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a9d1ece0d7e..7206d858dae 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -560,15 +560,19 @@ class MergeRequest < ActiveRecord::Base end def diff_refs - if persisted? - merge_request_diff.diff_refs - else - Gitlab::Diff::DiffRefs.new( - base_sha: diff_base_sha, - start_sha: diff_start_sha, - head_sha: diff_head_sha - ) - end + persisted? ? merge_request_diff.diff_refs : repository_diff_refs + end + + # Instead trying to fetch the + # persisted diff_refs, this method goes + # straight to the repository to get the + # most recent data possible. + def repository_diff_refs + Gitlab::Diff::DiffRefs.new( + base_sha: branch_merge_base_sha, + start_sha: target_branch_sha, + head_sha: source_branch_sha + ) end def branch_merge_base_sha diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 1ebcbcda0d8..b21edce3aad 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -45,7 +45,7 @@ class Milestone < ActiveRecord::Base groups = groups.compact if groups.is_a? Array groups = [] if groups.nil? - where(project: projects).or(where(group: groups)) + where(project_id: projects).or(where(group_id: groups)) end scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } @@ -191,7 +191,7 @@ class Milestone < ActiveRecord::Base return STATE_COUNT_HASH unless projects || groups counts = Milestone - .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id)) + .for_projects_and_groups(projects, groups) .reorder(nil) .group(:state) .count @@ -275,8 +275,7 @@ class Milestone < ActiveRecord::Base if project relation = Milestone.for_projects_and_groups([project_id], [project.group&.id]) elsif group - project_ids = group.projects.map(&:id) - relation = Milestone.for_projects_and_groups(project_ids, [group.id]) + relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id]) end title_exists = relation.find_by_title(title) diff --git a/app/models/project.rb b/app/models/project.rb index 7ab2fc30c24..15465d9b356 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -331,7 +331,7 @@ class Project < ActiveRecord::Base ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS }, enforce_user: true }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } - validate :check_limit, on: :create + validate :check_personal_projects_limit, on: :create validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) } validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) } @@ -809,18 +809,22 @@ class Project < ActiveRecord::Base ::Gitlab::CurrentSettings.mirror_available end - def check_limit - unless creator.can_create_project? || namespace.kind == 'group' - projects_limit = creator.projects_limit + def check_personal_projects_limit + # Since this method is called as validation hook, `creator` might not be + # present. Since the validation for that will fail, we can just return + # early. + return if !creator || creator.can_create_project? || + namespace.kind == 'group' - if projects_limit == 0 - self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions") + limit = creator.projects_limit + error = + if limit.zero? + _('Personal project creation is not allowed. Please contact your administrator with questions') else - self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it") + _('Your project limit is %{limit} projects! Please contact your administrator to increase it') end - end - rescue - self.errors.add(:base, "Can't check your ability to create project") + + self.errors.add(:limit_reached, error % { limit: limit }) end def visibility_level_allowed_by_group @@ -1531,7 +1535,7 @@ class Project < ActiveRecord::Base end def pages_available? - Gitlab.config.pages.enabled && !namespace.subgroup? + Gitlab.config.pages.enabled end def remove_private_deploy_keys @@ -1602,24 +1606,7 @@ class Project < ActiveRecord::Base # rubocop: disable CodeReuse/ServiceClass def after_create_default_branch - return unless default_branch - - # Ensure HEAD points to the default branch in case it is not master - change_head(default_branch) - - if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) - params = { - name: default_branch, - push_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }], - merge_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }] - } - - ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true) - end + Projects::ProtectDefaultBranchService.new(self).execute end # rubocop: enable CodeReuse/ServiceClass @@ -2039,6 +2026,10 @@ class Project < ActiveRecord::Base pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil) end + def link_pool_repository + pool_repository&.link_repository(repository) + end + private def merge_requests_allowing_collaboration(source_branch = nil) diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 525725034a5..aa0c121fe99 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -30,4 +30,8 @@ class ProjectImportData < ActiveRecord::Base def merge_credentials(hash) self.credentials = credentials.to_h.merge(hash) unless hash.empty? end + + def clear_credentials + self.credentials = {} + end end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index a3fa67c72bf..5eba7ddd75c 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -61,7 +61,10 @@ class RemoteMirror < ActiveRecord::Base timestamp = Time.now remote_mirror.update!( - last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil + last_update_at: timestamp, + last_successful_update_at: timestamp, + last_error: nil, + error_notification_sent: false ) end @@ -179,6 +182,10 @@ class RemoteMirror < ActiveRecord::Base project.repository.add_remote(remote_name, remote_url) end + def after_sent_notification + update_column(:error_notification_sent, true) + end + private def store_credentials @@ -221,7 +228,8 @@ class RemoteMirror < ActiveRecord::Base last_error: nil, last_update_at: nil, last_successful_update_at: nil, - update_status: 'finished' + update_status: 'finished', + error_notification_sent: false ) end diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb index c76b8e71507..7eee4fbbe5f 100644 --- a/app/models/suggestion.rb +++ b/app/models/suggestion.rb @@ -5,8 +5,7 @@ class Suggestion < ApplicationRecord validates :note, presence: true validates :commit_id, presence: true, if: :applied? - delegate :original_position, :position, :diff_file, - :noteable, to: :note + delegate :original_position, :position, :noteable, to: :note def project noteable.source_project @@ -16,6 +15,15 @@ class Suggestion < ApplicationRecord noteable.source_branch end + def file_path + position.file_path + end + + def diff_file + repository = project.repository + position.diff_file(repository) + end + # For now, suggestions only serve as a way to send patches that # will change a single line (being able to apply multiple in the same place), # which explains `from_line` and `to_line` being the same line. diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb index 7b65bd22f54..4744a7c1cc8 100644 --- a/app/serializers/base_serializer.rb +++ b/app/serializers/base_serializer.rb @@ -16,11 +16,11 @@ class BaseSerializer .as_json end - def self.entity(entity_class) - @entity_class ||= entity_class - end + class << self + attr_reader :entity_class - def self.entity_class - @entity_class + def entity(entity_class) + @entity_class ||= entity_class + end end end diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb index 433bfe60474..7e3053e5881 100644 --- a/app/serializers/merge_request_diff_entity.rb +++ b/app/serializers/merge_request_diff_entity.rb @@ -24,6 +24,14 @@ class MergeRequestDiffEntity < Grape::Entity short_sha(merge_request_diff.head_commit_sha) end + expose :base_version_path do |merge_request_diff| + project = merge_request.target_project + + next unless project + + merge_request_version_path(project, merge_request, merge_request_diff) + end + expose :version_path do |merge_request_diff| start_sha = options[:start_sha] project = merge_request.target_project diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb index e86ca8cf1d0..8a71730d5ec 100644 --- a/app/services/clusters/applications/base_helm_service.rb +++ b/app/services/clusters/applications/base_helm_service.rb @@ -45,6 +45,10 @@ module Clusters def install_command @install_command ||= app.install_command end + + def upgrade_command(new_values = "") + app.upgrade_command(new_values) + end end end end diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb new file mode 100644 index 00000000000..2683c75e41f --- /dev/null +++ b/app/services/import/base_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Import + class BaseService < ::BaseService + def initialize(client, user, params) + @client = client + @current_user = user + @params = params + end + + private + + def find_or_create_namespace(namespace, owner) + namespace = params[:target_namespace].presence || namespace + + return current_user.namespace if namespace == owner + + group = Groups::NestedCreateService.new(current_user, group_path: namespace).execute + + group.errors.any? ? current_user.namespace : group + rescue => e + Gitlab::AppLogger.error(e) + + current_user.namespace + end + + def project_save_error(project) + project.errors.full_messages.join(', ') + end + + def success(project) + super().merge(project: project, status: :success) + end + end +end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb new file mode 100644 index 00000000000..a2533683da9 --- /dev/null +++ b/app/services/import/github_service.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Import + class GithubService < Import::BaseService + attr_accessor :client + attr_reader :params, :current_user + + def execute(access_params, provider) + unless authorized? + return error('This namespace has already been taken! Please choose another one.', :unprocessable_entity) + end + + project = Gitlab::LegacyGithubImport::ProjectCreator + .new(repo, project_name, target_namespace, current_user, access_params, type: provider) + .execute(extra_project_attrs) + + if project.persisted? + success(project) + else + error(project_save_error(project), :unprocessable_entity) + end + end + + def repo + @repo ||= client.repo(params[:repo_id].to_i) + end + + def project_name + @project_name ||= params[:new_name].presence || repo.name + end + + def namespace_path + @namespace_path ||= params[:target_namespace].presence || current_user.namespace_path + end + + def target_namespace + @target_namespace ||= find_or_create_namespace(namespace_path, current_user.namespace_path) + end + + def extra_project_attrs + {} + end + + def authorized? + can?(current_user, :create_projects, target_namespace) + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index c7e7bb55e4b..805bb5b510d 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -61,10 +61,10 @@ class IssuableBaseService < BaseService return unless milestone_id params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE - group_ids = project.group&.self_and_ancestors&.pluck(:id) + groups = project.group&.self_and_ancestors&.select(:id) milestone = - Milestone.for_projects_and_groups([project.id], group_ids).find_by_id(milestone_id) + Milestone.for_projects_and_groups([project.id], groups).find_by_id(milestone_id) params[:milestone_id] = '' unless milestone end diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb index fe34be41ac1..db710bac900 100644 --- a/app/services/labels/create_service.rb +++ b/app/services/labels/create_service.rb @@ -3,7 +3,7 @@ module Labels class CreateService < Labels::BaseService def initialize(params = {}) - @params = params.dup.with_indifferent_access + @params = params.to_h.dup.with_indifferent_access end # returns the created label diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb index c3a720a1c66..e563447c64c 100644 --- a/app/services/labels/update_service.rb +++ b/app/services/labels/update_service.rb @@ -3,7 +3,7 @@ module Labels class UpdateService < Labels::BaseService def initialize(params = {}) - @params = params.dup.with_indifferent_access + @params = params.to_h.dup.with_indifferent_access end # returns the updated label diff --git a/app/services/lfs/locks_finder_service.rb b/app/services/lfs/locks_finder_service.rb index 4a5b2a52921..192ce3d3c2a 100644 --- a/app/services/lfs/locks_finder_service.rb +++ b/app/services/lfs/locks_finder_service.rb @@ -12,7 +12,7 @@ module Lfs # rubocop: disable CodeReuse/ActiveRecord def find_locks - options = params.slice(:id, :path).compact.symbolize_keys + options = params.slice(:id, :path).to_h.compact.symbolize_keys project.lfs_file_locks.where(options) end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index 39071b5dc14..cbe5996e8ca 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -82,11 +82,9 @@ module Milestones end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def group_project_ids - @group_project_ids ||= group.projects.pluck(:id) + group.projects.select(:id) end - # rubocop: enable CodeReuse/ActiveRecord def raise_error(message) raise PromoteMilestoneError, "Promotion failed - #{message}" diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 61f6402a810..3dad90188cf 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -14,7 +14,7 @@ module Projects order: { due_date: :asc, title: :asc } } - finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group + finder_params[:group_ids] = @project.group.self_and_ancestors.select(:id) if @project.group MilestonesFinder.new(finder_params).execute.select([:iid, :title]) end diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb index 8306d43ca7c..678bc0d24c3 100644 --- a/app/services/projects/create_from_template_service.rb +++ b/app/services/projects/create_from_template_service.rb @@ -5,7 +5,7 @@ module Projects include Gitlab::Utils::StrongMemoize def initialize(user, params) - @current_user, @params = user, params.dup + @current_user, @params = user, params.to_h.dup end def execute diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 336d029d330..b14b31302f5 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -7,9 +7,16 @@ module Projects DestroyError = Class.new(StandardError) DELETED_FLAG = '+deleted'.freeze + REPO_REMOVAL_DELAY = 5.minutes.to_i def async_execute project.update_attribute(:pending_delete, true) + + # Ensure no repository +deleted paths are kept, + # regardless of any issue with the ProjectDestroyWorker + # job process. + schedule_stale_repos_removal + job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.full_path} with job ID #{job_id}") end @@ -92,14 +99,23 @@ module Projects log_info(%Q{Repository "#{path}" moved to "#{new_path}" for project "#{project.full_path}"}) project.run_after_commit do - # self is now project - GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage, new_path) + GitlabShellWorker.perform_in(REPO_REMOVAL_DELAY, :remove_repository, self.repository_storage, new_path) end else false end end + def schedule_stale_repos_removal + repo_paths = [removal_path(repo_path), removal_path(wiki_path)] + + # Ideally it should wait until the regular removal phase finishes, + # so let's delay it a bit further. + repo_paths.each do |path| + GitlabShellWorker.perform_in(REPO_REMOVAL_DELAY * 2, :remove_repository, project.repository_storage, path) + end + end + def rollback_repository(old_path, new_path) # There is a possibility project does not have repository or wiki return true unless repo_exists?(old_path) @@ -113,13 +129,11 @@ module Projects end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def mv_repository(from_path, to_path) - return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git') + return true unless repo_exists?(from_path) gitlab_shell.mv_repository(project.repository_storage, from_path, to_path) end - # rubocop: enable CodeReuse/ActiveRecord def attempt_rollback(project, message) return unless project diff --git a/app/services/projects/protect_default_branch_service.rb b/app/services/projects/protect_default_branch_service.rb new file mode 100644 index 00000000000..245490791bf --- /dev/null +++ b/app/services/projects/protect_default_branch_service.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Projects + # Service class that can be used to execute actions necessary after creating a + # default branch. + class ProtectDefaultBranchService + attr_reader :project, :default_branch_protection + + # @param [Project] project + def initialize(project) + @project = project + + @default_branch_protection = Gitlab::Access::BranchProtection + .new(Gitlab::CurrentSettings.default_branch_protection) + end + + def execute + protect_default_branch if default_branch + end + + def protect_default_branch + # Ensure HEAD points to the default branch in case it is not master + project.change_head(default_branch) + + create_protected_branch if protect_branch? + end + + def create_protected_branch + params = { + name: default_branch, + push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }] + } + + # The creator of the project is always allowed to create protected + # branches, so we skip the authorization check in this service class. + ProtectedBranches::CreateService + .new(project, project.creator, params) + .execute(skip_authorization: true) + end + + def protect_branch? + default_branch_protection.any? && + !ProtectedBranch.protected?(project, default_branch) + end + + def default_branch + project.default_branch + end + + def push_access_level + if default_branch_protection.developer_can_push? + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MAINTAINER + end + end + + def merge_access_level + if default_branch_protection.developer_can_merge? + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MAINTAINER + end + end + end +end diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index d931d528c86..cc47b46b527 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -11,6 +11,10 @@ module Suggestions return error('Suggestion is not appliable') end + unless latest_diff_refs?(suggestion) + return error('The file has been changed') + end + params = file_update_params(suggestion) result = ::Files::UpdateService.new(suggestion.project, @current_user, params).execute @@ -19,30 +23,44 @@ module Suggestions end result + rescue Files::UpdateService::FileChangedError + error('The file has been changed') end private - def file_update_params(suggestion) - diff_file = suggestion.diff_file + # Checks whether the latest diff refs for the branch matches with + # the position refs we're using to update the file content. Since + # the persisted refs are updated async (for MergeRequest), + # it's more consistent to fetch this data directly from the repository. + def latest_diff_refs?(suggestion) + suggestion.position.diff_refs == suggestion.noteable.repository_diff_refs + end - file_path = diff_file.file_path - branch_name = suggestion.noteable.source_branch - file_content = new_file_content(suggestion) + def file_update_params(suggestion) + blob = suggestion.diff_file.new_blob + file_path = suggestion.file_path + branch_name = suggestion.branch + file_content = new_file_content(suggestion, blob) commit_message = "Apply suggestion to #{file_path}" + file_last_commit = + Gitlab::Git::Commit.last_for_path(suggestion.project.repository, + blob.commit_id, + blob.path) + { file_path: file_path, branch_name: branch_name, start_branch: branch_name, commit_message: commit_message, - file_content: file_content + file_content: file_content, + last_commit_sha: file_last_commit&.id } end - def new_file_content(suggestion) + def new_file_content(suggestion, blob) range = suggestion.from_line_index..suggestion.to_line_index - blob = suggestion.diff_file.new_blob blob.load_all_data! content = blob.data.lines diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb index af4fe1aebb9..0b00bd135eb 100644 --- a/app/services/users/update_service.rb +++ b/app/services/users/update_service.rb @@ -55,7 +55,7 @@ module Users params.reject! { |key, _| read_only.include?(key.to_sym) } end - @user.assign_attributes(params) if params.any? + @user.assign_attributes(params) unless params.empty? end end end diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 0efca895a50..9a243e07936 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -23,13 +23,23 @@ module RecordsUploads return unless model return unless file && file.exists? - Upload.transaction do - uploads.where(path: upload_path).delete_all - upload.delete if upload - - self.upload = build_upload.tap(&:save!) + # MySQL InnoDB may encounter a deadlock if a deletion and an + # insert is in the same transaction due to its next-key locking + # algorithm, so we need to skip the transaction. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55161#note_131556351 + if Gitlab::Database.mysql? + readd_upload + else + Upload.transaction { readd_upload } end end + + def readd_upload + uploads.where(path: upload_path).delete_all + upload.delete if upload + + self.upload = build_upload.tap(&:save!) + end # rubocop: enable CodeReuse/ActiveRecord def upload_path diff --git a/app/views/clusters/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml index 5e451f60c9d..4c47e11927e 100644 --- a/app/views/clusters/clusters/_integration_form.html.haml +++ b/app/views/clusters/clusters/_integration_form.html.haml @@ -13,19 +13,19 @@ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') - - if has_multiple_clusters? - .form-group - %h5= s_('ClusterIntegration|Environment scope') + .form-group + %h5= s_('ClusterIntegration|Environment scope') + - if has_multiple_clusters? = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") + - else + = text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true + - environment_scope_url = 'https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope-premium' + - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url } + .form-text.text-muted + %code * + = s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe } - if can?(current_user, :update_cluster, @cluster) .form-group = field.submit _('Save changes'), class: 'btn btn-success' - - - unless has_multiple_clusters? - %h5= s_('ClusterIntegration|Environment scope') - %p - %code * - is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. - = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope') diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 1050945b15a..ae67192cbcd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -15,9 +15,11 @@ = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your projects + %span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count) = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do Starred projects + %span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count) = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, data: {placement: 'right'} do Explore projects diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml index 04409408ce0..513902890af 100644 --- a/app/views/layouts/header/_help_dropdown.html.haml +++ b/app/views/layouts/header/_help_dropdown.html.haml @@ -1,9 +1,13 @@ +- show_blog_link = current_user_menu?(:help) && blog_post_url.present? %ul - if current_user_menu?(:help) %li = link_to _("Help"), help_path %li.divider + - if show_blog_link %li - = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback" + = link_to _("What's new?"), blog_post_url + %li + = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback" - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) = render 'shared/user_dropdown_contributing_link' diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index d62cbc1684b..1eab7813865 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -43,7 +43,7 @@ - if project_nav_tab? :files = nav_link(controller: sidebar_repository_paths) do - = link_to project_tree_path(@project), class: 'shortcuts-tree' do + = link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do .nav-icon-container = sprite_icon('doc-text') %span.nav-item-name @@ -64,7 +64,7 @@ = _('Commits') = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do + = link_to project_branches_path(@project), class: 'qa-branches-link' do = _('Branches') = nav_link(controller: [:tags]) do @@ -201,7 +201,7 @@ - if project_nav_tab? :operations = nav_link(controller: sidebar_operations_paths) do - = link_to sidebar_operations_link_path, class: 'shortcuts-operations' do + = link_to sidebar_operations_link_path, class: 'shortcuts-operations qa-link-operations' do .nav-icon-container = sprite_icon('cloud-gear') %span.nav-item-name @@ -227,7 +227,7 @@ %span = _('Environments') - - if project_nav_tab?(:error_tracking) && Feature.enabled?(:error_tracking, @project) + - if project_nav_tab?(:error_tracking) = nav_link(controller: :error_tracking) do = link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do %span diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index e167e094240..ee2c5a13b8a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,37 +1,37 @@ -- page_title "Account" +- page_title _('Account') - @content_class = "limit-container-width" unless fluid_layout - if current_user.ldap_user? .alert.alert-info - Some options are unavailable for LDAP accounts + = s_('Profiles|Some options are unavailable for LDAP accounts') .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Two-Factor Authentication + = s_('Profiles|Two-Factor Authentication') %p - Increase your account's security by enabling Two-Factor Authentication (2FA). + = s_("Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)") .col-lg-8 %p - Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} + #{_('Status')}: #{current_user.two_factor_enabled? ? _('Enabled') : _('Disabled')} - if current_user.two_factor_enabled? - = link_to 'Manage two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-info' + = link_to _('Manage two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-info' - else .append-bottom-10 - = link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success' + = link_to _('Enable two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-success' %hr - if display_providers_on_profile? .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Social sign-in + = s_('Profiles|Social sign-in') %p - Activate signin with one of the following services + = s_('Profiles|Activate signin with one of the following services') .col-lg-8 %label.label-bold - Connected Accounts - %p Click on icon to activate signin with one of the following services + = s_('Profiles|Connected Accounts') + %p= s_('Profiles|Click on icon to activate signin with one of the following services') - button_based_providers.each do |provider| .provider-btn-group .provider-btn-image @@ -39,24 +39,24 @@ - if auth_active?(provider) - if unlink_allowed?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do - Disconnect + = s_('Profiles|Disconnect') - else %a.provider-btn - Active + = s_('Profiles|Active') - else = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do - Connect + = s_('Profiles|Connect') = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities] %hr - if current_user.can_change_username? .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0.warning-title - Change username + = s_('Profiles|Change username') %p - Changing your username can have unintended side effects. + = s_('Profiles|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' + = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank' .col-lg-8 - data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) } #update-username{ data: data } diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index d2587af11dd..e8cc3d6bcf0 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -23,7 +23,7 @@ = sprite_icon('tag', size: 16, css_class: 'icon append-right-4') = @project.topics_to_show - if @project.has_extra_topics? - = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown } + = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown } .project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml index cfb91568061..f42d5128715 100644 --- a/app/views/projects/artifacts/_tree_file.html.haml +++ b/app/views/projects/artifacts/_tree_file.html.haml @@ -14,4 +14,4 @@ = link_to path_to_file, class: 'str-truncated' do %span= blob.name %td - = number_to_human_size(blob.size, precision: 2) + = number_to_human_size(blob.size) diff --git a/app/views/projects/badges/badge_flat-square.svg.erb b/app/views/projects/badges/badge_flat-square.svg.erb new file mode 100644 index 00000000000..5b90da15ef5 --- /dev/null +++ b/app/views/projects/badges/badge_flat-square.svg.erb @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= badge.width %>" height="20"> + <g shape-rendering="crispEdges"> + <path fill="<%= badge.key_color %>" d="M0 0 h<%= badge.key_width %> v20 H0 z"/> + <path fill="<%= badge.value_color %>" d="M<%= badge.key_width %> 0 h<%= badge.value_width %> v20 H<%= badge.key_width %> z"/> + </g> + + <g fill="#fff" text-anchor="middle"> + <g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> + <text x="<%= badge.key_text_anchor %>" y="14"> + <%= badge.key_text %> + </text> + <text x="<%= badge.value_text_anchor %>" y="14"> + <%= badge.value_text %> + </text> + </g> + </g> +</svg> diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 88f9b7dfc9f..4b0ea15335e 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -76,7 +76,7 @@ = icon("trash-o") - else = link_to project_branch_path(@project, branch.name), - class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", + class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip", title: s_('Branches|Delete branch'), method: :delete, data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } }, diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml index 0e4b119bb54..93061452e12 100644 --- a/app/views/projects/branches/_panel.html.haml +++ b/app/views/projects/branches/_panel.html.haml @@ -10,7 +10,7 @@ .card.prepend-top-10 .card-header = panel_title - %ul.content-list.all-branches + %ul.content-list.all-branches.qa-all-branches - branches.first(overview_max_branches).each do |branch| = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch) - if branches.size > overview_max_branches diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index ca867961f6b..43f1cd01b67 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -35,7 +35,7 @@ - if can? current_user, :push_code, @project = link_to project_merged_branches_path(@project), - class: 'btn btn-inverted btn-remove has-tooltip', + class: 'btn btn-inverted btn-remove has-tooltip qa-delete-merged-branches', 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?'), diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index b7e1cf85cb7..aebd176af9b 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -1,8 +1,5 @@ - @no_container = true - page_title _("Environments") -#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json), - "folder-name" => @folder, - "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, - "can-read-environment" => can?(current_user, :read_environment, @project).to_s, +#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data, "css-class" => container_class } } diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index fd6559e37ba..329efa0cdbf 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,5 +1,5 @@ - show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true) -- show_import_button = local_assigns.fetch(:show_import_button, true) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project) +- show_import_button = local_assigns.fetch(:show_import_button, true) && can?(current_user, :import_issues, @project) - show_export_button = local_assigns.fetch(:show_export_button, true) .nav-controls.issues-nav-controls diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index 59592abcf6a..afea5268006 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -8,10 +8,6 @@ .nav-controls - if can?(current_user, :update_build, @project) - - if @all_builds.running_or_pending.limit(1).any? # rubocop: disable CodeReuse/ActiveRecord - = link_to 'Cancel running', cancel_all_project_jobs_path(@project), - data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - - unless @repository.gitlab_ci_yml = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index 3f05e06b0c6..b5d397e3065 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -1,7 +1,6 @@ .card.project-members-groups .card-header - Groups with access to - %strong= @project.name + = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(@project.name) } %span.badge.badge-pill= group_links.size %ul.content-list.members-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_new_project_group.html.haml b/app/views/projects/project_members/_new_project_group.html.haml index 88e68f89024..079811e4e79 100644 --- a/app/views/projects/project_members/_new_project_group.html.haml +++ b/app/views/projects/project_members/_new_project_group.html.haml @@ -10,8 +10,9 @@ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - = link_to _("Read more"), help_page_path("user/permissions") - about role permissions + - permissions_docs_path = help_page_path('user/permissions') + - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path } + = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } .form-group = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' .clearable-input diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 1de7d9c6957..0590578c3fe 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -2,20 +2,21 @@ .col-sm-12 = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| .form-group - = label_tag :user_ids, "Select members to invite", class: "label-bold" + = label_tag :user_ids, _("Select members to invite"), class: "label-bold" = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group - = label_tag :access_level, "Choose a role permission", class: "label-bold" + = label_tag :access_level, _("Choose a role permission"), class: "label-bold" .select-wrapper = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions") - about role permissions + - permissions_docs_path = help_page_path('user/permissions') + - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path } + = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } .form-group .clearable-input - = label_tag :expires_at, 'Access expiration date', class: 'label-bold' + = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - = f.submit "Add to project", class: "btn btn-success qa-add-member-button" - = link_to "Import", import_project_project_members_path(@project), class: "btn btn-default", title: "Import members from another project" + = f.submit _("Add to project"), class: "btn btn-success qa-add-member-button" + = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project") diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 9682f8ac922..b92ecf4412f 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -4,14 +4,13 @@ .card .card-header.flex-project-members-panel %span.flex-project-title - Members of - %strong= project.name + = _("Members of <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(project.name) } %span.badge.badge-pill= members.total_count = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group .position-relative - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => _("Submit search") } = icon("search") = render 'shared/members/sort_dropdown' %ul.content-list.members-list.qa-members-list diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 8b93e81cd31..bcca943de6a 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -1,15 +1,15 @@ -- page_title "Import members" +- page_title _("Import members") %h3.page-title - Import members from another project + = _("Import members from another project") %p.light - Only project members will be imported. Group members will be skipped. + = _("Only project members will be imported. Group members will be skipped.") %hr = form_tag apply_import_project_project_members_path(@project), method: 'post' do .form-group.row - = label_tag :source_project_id, "Project", class: 'col-form-label col-sm-2' + = label_tag :source_project_id, _("Project"), class: 'col-form-label col-sm-2' .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions - = button_tag 'Import project members', class: "btn btn-success" - = link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel" + = button_tag _('Import project members'), class: "btn btn-success" + = link_to _("Cancel"), project_project_members_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 14ed3345765..242ff91f539 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,39 +1,34 @@ -- page_title "Members" +- page_title _("Members") .row.prepend-top-default .col-lg-12 %h4 - Project members + = _("Project members") - if can?(current_user, :admin_project_member, @project) %p - You can invite a new member to - %strong= @project.name - or invite another group. + = _("You can invite a new member to <strong>%{project_name}</strong> or invite another group.").html_safe % { project_name: sanitize_project_name(@project.name) } - else %p - Members can be added by project - %i Maintainers - or - %i Owners + = _("Members can be added by project <i>Maintainers</i> or <i>Owners</i>").html_safe .light - if can?(current_user, :admin_project_member, @project) %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } %li.nav-tab{ role: 'presentation' } - %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member + %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member") - if @project.allowed_to_share_with_group? %li.nav-tab{ role: 'presentation' } - %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group + %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite group") .tab-content.gitlab-tab-content .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_member', tab_title: 'Invite member' + = render 'projects/project_members/new_project_member', tab_title: _('Invite member') .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_group', tab_title: 'Invite group' + = render 'projects/project_members/new_project_group', tab_title: _('Invite group') = render 'shared/members/requests', membership_source: @project, requesters: @requesters .clearfix %h5.member.existing-title - Existing members and groups + = _("Existing members and groups") - if @group_links.any? = render 'projects/project_members/groups', group_links: @group_links diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml index 871b60f05ba..4911e8d3770 100644 --- a/app/views/projects/settings/operations/_error_tracking.html.haml +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -1,4 +1,4 @@ -- return unless Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project) +- return unless can?(current_user, :read_environment, @project) - setting = error_tracking_setting diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index ff9a7b53a86..aaf9b973cda 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -6,75 +6,75 @@ - if project_search_tabs?(:blobs) %li{ class: active_when(@scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do - Code + = _("Code") %span.badge.badge-pill = @search_results.blobs_count - if project_search_tabs?(:issues) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do - Issues + = _("Issues") %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) - if project_search_tabs?(:merge_requests) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do - Merge requests + = _("Merge requests") %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) - if project_search_tabs?(:milestones) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do - Milestones + = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) - if project_search_tabs?(:notes) %li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'notes') do - Comments + = _("Comments") %span.badge.badge-pill = limited_count(@search_results.limited_notes_count) - if project_search_tabs?(:wiki) %li{ class: active_when(@scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do - Wiki + = _("Wiki") %span.badge.badge-pill = @search_results.wiki_blobs_count - if project_search_tabs?(:commits) %li{ class: active_when(@scope == 'commits') } = link_to search_filter_path(scope: 'commits') do - Commits + = _("Commits") %span.badge.badge-pill = @search_results.commits_count - elsif @show_snippets %li{ class: active_when(@scope == 'snippet_blobs') } = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - Snippet Contents + = _("Snippet Contents") %span.badge.badge-pill = @search_results.snippet_blobs_count %li{ class: active_when(@scope == 'snippet_titles') } = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - Titles and Filenames + = _("Titles and Filenames") %span.badge.badge-pill = @search_results.snippet_titles_count - else %li{ class: active_when(@scope == 'projects') } = link_to search_filter_path(scope: 'projects') do - Projects + = _("Projects") %span.badge.badge-pill = limited_count(@search_results.limited_projects_count) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do - Issues + = _("Issues") %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do - Merge requests + = _("Merge requests") %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do - Milestones + = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index 6837f64f132..c8b6a3258ab 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -3,31 +3,31 @@ - if params[:project_id].present? = hidden_field_tag :project_id, params[:project_id] .dropdown - %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:", group_id: params[:group_id] } } + %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Group:'), group_id: params[:group_id] } } %span.dropdown-toggle-text - Group: + = _("Group:") - if @group.present? = @group.name - else - Any + = _("Any") = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right - = dropdown_title("Filter results by group") - = dropdown_filter("Search groups") + = dropdown_title(_("Filter results by group")) + = dropdown_filter(_("Search groups")) = dropdown_content = dropdown_loading .dropdown.project-filter - %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } + %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Project:') } } %span.dropdown-toggle-text - Project: + = _("Project:") - if @project.present? = @project.full_name - else - Any + = _("Any") = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right - = dropdown_title("Filter results by project") - = dropdown_filter("Search projects") + = dropdown_title(_("Filter results by project")) + = dropdown_filter(_("Search projects")) = dropdown_content = dropdown_loading diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index a4a5cec1314..4af0c6bf84a 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -4,12 +4,12 @@ .search-holder .search-field-holder - = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false + = search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false = icon("search", class: "search-icon") %button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" } = icon("times-circle") %span.sr-only - Clear search + = _("Clear search") - unless params[:snippets].eql? 'true' = render 'filter' - = button_tag "Search", class: "btn btn-success btn-search" + = button_tag _("Search"), class: "btn btn-success btn-search" diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index c4d52431d6e..be7a2436d16 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -6,9 +6,11 @@ = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project - in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]} + - link_to_project = link_to(@project.full_name, [@project.namespace.becomes(Namespace), @project]) + = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project } - elsif @group - in group #{link_to @group.name, @group} + - link_to_group = link_to(@group.name, @group) + = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } .results.prepend-top-10 - if @scope == 'commits' diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml index 821a39d61f5..9d15995bb51 100644 --- a/app/views/search/results/_empty.html.haml +++ b/app/views/search/results/_empty.html.haml @@ -2,5 +2,5 @@ .search_glyph %h4 = icon('search') - We couldn't find any results matching + = _("We couldn't find any results matching") %code= @search_term diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index c413c3d4abb..796782035f2 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -4,7 +4,7 @@ = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do %span.term.str-truncated= issue.title - if issue.closed? - %span.badge.badge-danger.prepend-left-5 Closed + %span.badge.badge-danger.prepend-left-5= _("Closed") .float-right ##{issue.iid} - if issue.description.present? .description.term diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 519176af108..f0e0af11f27 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -3,9 +3,9 @@ = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - if merge_request.merged? - %span.badge.badge-primary.prepend-left-5 Merged + %span.badge.badge-primary.prepend-left-5= _("Merged") - elsif merge_request.closed? - %span.badge.badge-danger.prepend-left-5 Closed + %span.badge.badge-danger.prepend-left-5= _("Closed") .float-right= merge_request.to_reference - if merge_request.description.present? .description.term diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index e4ab7b0541f..6717939d034 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -6,14 +6,14 @@ %h5.note-search-caption.str-truncated %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) - commented on - = link_to project.full_name, project + - link_to_project = link_to(project.full_name, project) + = _("commented on %{link_to_project}").html_safe % { link_to_project: link_to_project } · - if note.for_commit? - = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do + = link_to_if(noteable_identifier, _("Commit %{commit_id}") % { commit_id: truncate_sha(note.commit_id) }, note_url) do = truncate_sha(note.commit_id) - %span.light Commit deleted + %span.light= _("Commit deleted") - else %span #{note.noteable_type.titleize} ##{noteable_identifier} diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index b4ecd7bbae9..e0130f9a4b5 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -24,7 +24,7 @@ = markup(snippet.file_name, chunk[:data], legacy_render_context(params)) - else .file-content.code - .nothing-here-block Empty file + .nothing-here-block= _("Empty file") - else .file-content.code.js-syntax-highlight .line-numbers @@ -42,4 +42,4 @@ = highlight(snippet.file_name, chunk[:data]) - else .file-content.code - .nothing-here-block Empty file + .nothing-here-block= _("Empty file") diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 9c8afb2165b..1e01088d9e6 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -5,7 +5,7 @@ - if snippet_title.private? %span.badge.badge-gray %i.fa.fa-lock - private + = _("private") %span.cgray.monospace.tiny.float-right.term = snippet_title.file_name diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 499697f2777..3260d05f509 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,5 +1,5 @@ - @hide_top_links = true -- breadcrumb_title "Search" +- breadcrumb_title _("Search") - page_title @search_term .prepend-top-default diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index 28b34e38b15..8607f87ce0b 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -7,7 +7,6 @@ .stage-container.dropdown{ class: klass } %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } = sprite_icon(icon_status) - = icon('caret-down') %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container %li.js-builds-dropdown-list.scrollable-menu diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml index 3150d39b84a..a8d3de66418 100644 --- a/app/views/shared/_personal_access_tokens_created_container.html.haml +++ b/app/views/shared/_personal_access_tokens_created_container.html.haml @@ -6,7 +6,7 @@ = container_title .form-group .input-group - = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block" + = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "qa-created-personal-access-token form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block" %span.input-group-append = clipboard_button(text: new_token_value, title: clipboard_button_title, placement: "left", class: "input-group-text btn-default btn-clipboard") %span#created-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again. diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index f4df7bdcd83..0891b3459ec 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -12,7 +12,7 @@ .row .form-group.col-md-6 = f.label :name, class: 'label-bold' - = f.text_field :name, class: "form-control", required: true + = f.text_field :name, class: "form-control qa-personal-access-token-name-field", required: true .row .form-group.col-md-6 @@ -26,4 +26,4 @@ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes .prepend-top-default - = f.submit "Create #{type} token", class: "btn btn-success" + = f.submit "Create #{type} token", class: "btn btn-success qa-create-token-button" diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml index 2efd03d4867..49f3aae0f98 100644 --- a/app/views/shared/_personal_access_tokens_table.html.haml +++ b/app/views/shared/_personal_access_tokens_table.html.haml @@ -29,7 +29,7 @@ %span.token-never-expires-label Never %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token) - %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } + %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right qa-revoke-button", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } - else .settings-message.text-center This user has no active #{type} Tokens. diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index c6c5cadc3f5..307a0919a4c 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -1,6 +1,6 @@ .board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }', ":data-id" => "list.id" } - .board-inner + .board-inner.d-flex.flex-column %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" } %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' } %i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable", diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 2691ec4cd46..9173b802dd4 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -1,6 +1,6 @@ - button_path = local_assigns.fetch(:button_path, false) - project_select_button = local_assigns.fetch(:project_select_button, false) -- show_import_button = local_assigns.fetch(:show_import_button, false) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project) +- show_import_button = local_assigns.fetch(:show_import_button, false) && can?(current_user, :import_issues, @project) - has_button = button_path || project_select_button - closed_issues_count = issuables_count_for_state(:issues, :closed) - opened_issues_count = issuables_count_for_state(:issues, :opened) diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index bee26cd8312..a739103641e 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -1,6 +1,6 @@ .row.empty-state.labels .col-12 - .svg-content + .svg-content.qa-label-svg = image_tag 'illustrations/labels.svg' .col-12 .text-content diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index af9db5f59a8..a5d3e1c8de0 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -4,6 +4,6 @@ - scopes.each do |scope| %fieldset.form-group.form-check - = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input' + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio" = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-bold form-check-label' .text-secondary= t scope, scope: [:doorkeeper, :scope_desc] diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index d3628b23189..b33e9b1f718 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -23,6 +23,7 @@ class GitGarbageCollectWorker end task = task.to_sym + project.link_pool_repository gitaly_call(task, project.repository.raw_repository) # Refresh the branch cache in case garbage collection caused a ref lookup to fail diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb index 07676011b2a..9c5161fd55a 100644 --- a/app/workers/object_pool/join_worker.rb +++ b/app/workers/object_pool/join_worker.rb @@ -5,14 +5,13 @@ module ObjectPool include ApplicationWorker include ObjectPoolQueue - def perform(pool_id, project_id) - pool = PoolRepository.find_by_id(pool_id) - return unless pool&.joinable? - + # The use of pool id is deprecated. Keeping the argument allows old jobs to + # still be performed. + def perform(_pool_id, project_id) project = Project.find_by_id(project_id) - return unless project + return unless project&.pool_repository&.joinable? - pool.link_repository(project.repository) + project.link_pool_repository Projects::HousekeepingService.new(project).execute end diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb index 70c2e857d09..5bafe8e2046 100644 --- a/app/workers/remote_mirror_notification_worker.rb +++ b/app/workers/remote_mirror_notification_worker.rb @@ -9,7 +9,10 @@ class RemoteMirrorNotificationWorker # We check again if there's an error because a newer run since this job was # fired could've completed successfully. return unless remote_mirror && remote_mirror.last_error.present? + return if remote_mirror.error_notification_sent? NotificationService.new.remote_mirror_update_failed(remote_mirror) + + remote_mirror.after_sent_notification end end diff --git a/bin/changelog b/bin/changelog index 758c036161e..328d9495b96 100755 --- a/bin/changelog +++ b/bin/changelog @@ -148,7 +148,7 @@ class ChangelogEntry def execute assert_feature_branch! - assert_title! + assert_title! unless editor assert_new_file! # Read type from $stdin unless is already set @@ -162,6 +162,10 @@ class ChangelogEntry write amend_commit if options.amend end + + if editor + system("#{editor} '#{file_path}'") + end end private @@ -180,6 +184,10 @@ class ChangelogEntry File.write(file_path, contents) end + def editor + ENV['EDITOR'] + end + def amend_commit fail_with "git add failed" unless system(*%W[git add #{file_path}]) diff --git a/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml b/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml new file mode 100644 index 00000000000..da1777827cb --- /dev/null +++ b/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Add What's new menu item in top navigation +merge_request: 23186 +author: +type: added diff --git a/changelogs/unreleased/26375-markdown-footnotes-not-working.yml b/changelogs/unreleased/26375-markdown-footnotes-not-working.yml new file mode 100644 index 00000000000..86adef84a2a --- /dev/null +++ b/changelogs/unreleased/26375-markdown-footnotes-not-working.yml @@ -0,0 +1,5 @@ +--- +title: Footnotes now render properly in markdown +merge_request: 24168 +author: +type: fixed diff --git a/changelogs/unreleased/30120-add-flat-square-badge-style.yml b/changelogs/unreleased/30120-add-flat-square-badge-style.yml new file mode 100644 index 00000000000..a542a58d3fc --- /dev/null +++ b/changelogs/unreleased/30120-add-flat-square-badge-style.yml @@ -0,0 +1,5 @@ +--- +title: Add flat-square badge style +merge_request: 24172 +author: Fabian Schneider @fabsrc +type: added diff --git a/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml b/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml new file mode 100644 index 00000000000..5a4ff8b3358 --- /dev/null +++ b/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Remove expansion hover animation from pipeline status icon buttons +merge_request: 24268 +author: Nathan Friend +type: changed diff --git a/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml b/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml new file mode 100644 index 00000000000..d1a80ab43cf --- /dev/null +++ b/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml @@ -0,0 +1,5 @@ +--- +title: Improve milestone queries using subqueries instead of separate queries for ids +merge_request: 24325 +author: +type: performance diff --git a/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml b/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml new file mode 100644 index 00000000000..c1cde0ceff6 --- /dev/null +++ b/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml @@ -0,0 +1,5 @@ +--- +title: Resolve In Merge Request diff screen, master is not a hyperlink +merge_request: 23874 +author: +type: fixed diff --git a/changelogs/unreleased/52278-squash-checkbox-fix.yml b/changelogs/unreleased/52278-squash-checkbox-fix.yml new file mode 100644 index 00000000000..c81748ae419 --- /dev/null +++ b/changelogs/unreleased/52278-squash-checkbox-fix.yml @@ -0,0 +1,5 @@ +--- +title: Resolve When merging an MR, the squash checkbox isnt always supported +merge_request: 24296 +author: +type: fixed diff --git a/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml b/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml new file mode 100644 index 00000000000..07cb35e6529 --- /dev/null +++ b/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml @@ -0,0 +1,5 @@ +--- +title: Modifies environment scope UI on cluster page +merge_request: 24376 +author: +type: other diff --git a/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml b/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml new file mode 100644 index 00000000000..b609fc2d60b --- /dev/null +++ b/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Fix default_branch_protection admin setting' +merge_request: 24398 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/55945-suggested-change-highlight.yml b/changelogs/unreleased/55945-suggested-change-highlight.yml deleted file mode 100644 index 611854d36ab..00000000000 --- a/changelogs/unreleased/55945-suggested-change-highlight.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add syntax highlighting to suggestion diff -merge_request: 24156 -author: -type: fixed diff --git a/changelogs/unreleased/55945-suggested-change-preview-highlight.yml b/changelogs/unreleased/55945-suggested-change-preview-highlight.yml new file mode 100644 index 00000000000..997290a5d50 --- /dev/null +++ b/changelogs/unreleased/55945-suggested-change-preview-highlight.yml @@ -0,0 +1,5 @@ +--- +title: Fix syntax highlighting for suggested changes preview +merge_request: 24358 +author: +type: fixed diff --git a/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml b/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml new file mode 100644 index 00000000000..7c923422534 --- /dev/null +++ b/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml @@ -0,0 +1,6 @@ +--- +title: Show CI artifact file size with 3 significant digits on 'browse job artifacts' + page +merge_request: 24387 +author: +type: fixed diff --git a/changelogs/unreleased/56389-remove-unwanted-suggestion-flash-margin.yml b/changelogs/unreleased/56389-remove-unwanted-suggestion-flash-margin.yml new file mode 100644 index 00000000000..3494feb9be1 --- /dev/null +++ b/changelogs/unreleased/56389-remove-unwanted-suggestion-flash-margin.yml @@ -0,0 +1,5 @@ +--- +title: Remove unwanted margin above suggested changes. +merge_request: 24419 +author: +type: fixed diff --git a/changelogs/unreleased/56507-sh-bump-katex-0.10.0.yml b/changelogs/unreleased/56507-sh-bump-katex-0.10.0.yml new file mode 100644 index 00000000000..671e204da21 --- /dev/null +++ b/changelogs/unreleased/56507-sh-bump-katex-0.10.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade KaTeX to version 0.10.0 +merge_request: 24478 +author: Andrew Harmon +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/56622-admin-settings-cannot-read-property-addeventlistener-of-null.yml b/changelogs/unreleased/56622-admin-settings-cannot-read-property-addeventlistener-of-null.yml new file mode 100644 index 00000000000..52b2db0e999 --- /dev/null +++ b/changelogs/unreleased/56622-admin-settings-cannot-read-property-addeventlistener-of-null.yml @@ -0,0 +1,5 @@ +--- +title: Load initUserInternalRegexPlaceholder only when required +merge_request: 24522 +author: +type: fixed diff --git a/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml b/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml new file mode 100644 index 00000000000..cd7b56a1e05 --- /dev/null +++ b/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml @@ -0,0 +1,5 @@ +--- +title: Creates mixin to reduce code duplication between CE and EE in graph component +merge_request: +author: +type: other diff --git a/changelogs/unreleased/Projects--dropdown-is-misaligned-on-issue-boards-page.yml b/changelogs/unreleased/Projects--dropdown-is-misaligned-on-issue-boards-page.yml new file mode 100644 index 00000000000..49511294c48 --- /dev/null +++ b/changelogs/unreleased/Projects--dropdown-is-misaligned-on-issue-boards-page.yml @@ -0,0 +1,5 @@ +--- +title: Proper align Projects dropdown on issue boards page +merge_request: 24277 +author: Johann Hubert Sonntagbauer +type: fixed diff --git a/changelogs/unreleased/ac-pages-subgroups.yml b/changelogs/unreleased/ac-pages-subgroups.yml new file mode 100644 index 00000000000..ef5a0c1872e --- /dev/null +++ b/changelogs/unreleased/ac-pages-subgroups.yml @@ -0,0 +1,5 @@ +--- +title: Pages for subgroups +merge_request: 23505 +author: +type: added diff --git a/changelogs/unreleased/actioncontroller-parameters-deprecations.yml b/changelogs/unreleased/actioncontroller-parameters-deprecations.yml new file mode 100644 index 00000000000..ddd15c37542 --- /dev/null +++ b/changelogs/unreleased/actioncontroller-parameters-deprecations.yml @@ -0,0 +1,5 @@ +--- +title: Fix several ActionController::Parameters deprecations +merge_request: 24332 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml b/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml new file mode 100644 index 00000000000..e200bbaa806 --- /dev/null +++ b/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml @@ -0,0 +1,5 @@ +--- +title: Add badge count to projects +merge_request: 18425 +author: George Tsiolis +type: added diff --git a/changelogs/unreleased/an-dtracing-test-for-invalid-tracers.yml b/changelogs/unreleased/an-dtracing-test-for-invalid-tracers.yml new file mode 100644 index 00000000000..5365260cbae --- /dev/null +++ b/changelogs/unreleased/an-dtracing-test-for-invalid-tracers.yml @@ -0,0 +1,5 @@ +--- +title: Avoid overwriting default jaeger values with nil +merge_request: 24482 +author: +type: fixed diff --git a/changelogs/unreleased/an-gilab-process-name.yml b/changelogs/unreleased/an-gilab-process-name.yml new file mode 100644 index 00000000000..72d811ee21f --- /dev/null +++ b/changelogs/unreleased/an-gilab-process-name.yml @@ -0,0 +1,5 @@ +--- +title: Extract process_name from GitLab::Sentry +merge_request: 24422 +author: +type: other diff --git a/changelogs/unreleased/an-opentracing-factory.yml b/changelogs/unreleased/an-opentracing-factory.yml new file mode 100644 index 00000000000..c04736f3e63 --- /dev/null +++ b/changelogs/unreleased/an-opentracing-factory.yml @@ -0,0 +1,5 @@ +--- +title: Conditionally initialize the global opentracing tracer +merge_request: 24186 +author: +type: other diff --git a/changelogs/unreleased/api-nested-group-permission.yml b/changelogs/unreleased/api-nested-group-permission.yml new file mode 100644 index 00000000000..3ec0df6893f --- /dev/null +++ b/changelogs/unreleased/api-nested-group-permission.yml @@ -0,0 +1,5 @@ +--- +title: Return the maximum group access level in the projects API +merge_request: 24403 +author: +type: changed diff --git a/changelogs/unreleased/api-tags-search.yml b/changelogs/unreleased/api-tags-search.yml new file mode 100644 index 00000000000..1501acd5a9e --- /dev/null +++ b/changelogs/unreleased/api-tags-search.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Support searching for tags' +merge_request: 24385 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/api-wiki-dot-slug.yml b/changelogs/unreleased/api-wiki-dot-slug.yml new file mode 100644 index 00000000000..82c76fa7450 --- /dev/null +++ b/changelogs/unreleased/api-wiki-dot-slug.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Support dots in wiki slugs' +merge_request: 24383 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/auto-devops-custom-domains.yml b/changelogs/unreleased/auto-devops-custom-domains.yml new file mode 100644 index 00000000000..37e8ee26a4d --- /dev/null +++ b/changelogs/unreleased/auto-devops-custom-domains.yml @@ -0,0 +1,5 @@ +--- +title: Added support for custom hosts/domains to Auto DevOps +merge_request: 24248 +author: walkafwalka +type: added diff --git a/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml b/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml new file mode 100644 index 00000000000..1a8cdead4ac --- /dev/null +++ b/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml @@ -0,0 +1,5 @@ +--- +title: Bump kubectl in Auto DevOps to 1.11.6 +merge_request: 24176 +author: +type: other diff --git a/changelogs/unreleased/backup_restore_fix_issue_46891.yml b/changelogs/unreleased/backup_restore_fix_issue_46891.yml new file mode 100644 index 00000000000..b8fe3b1b861 --- /dev/null +++ b/changelogs/unreleased/backup_restore_fix_issue_46891.yml @@ -0,0 +1,5 @@ +--- +title: Modify file restore to rectify tar issue +merge_request: 24000 +author: +type: fixed diff --git a/changelogs/unreleased/bump-ingress-chart-112.yml b/changelogs/unreleased/bump-ingress-chart-112.yml new file mode 100644 index 00000000000..8a46fedb4b0 --- /dev/null +++ b/changelogs/unreleased/bump-ingress-chart-112.yml @@ -0,0 +1,5 @@ +--- +title: Bump nginx-ingress chart to 1.1.2 +merge_request: 24203 +author: +type: other diff --git a/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml b/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml new file mode 100644 index 00000000000..6e8dac97249 --- /dev/null +++ b/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup legacy artifact background migration +merge_request: 24144 +author: +type: other diff --git a/changelogs/unreleased/diff-tree-collapse-directories.yml b/changelogs/unreleased/diff-tree-collapse-directories.yml new file mode 100644 index 00000000000..6eae48f2352 --- /dev/null +++ b/changelogs/unreleased/diff-tree-collapse-directories.yml @@ -0,0 +1,5 @@ +--- +title: Collapse directory structure in merge request file tree +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml b/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml new file mode 100644 index 00000000000..b224cace4bf --- /dev/null +++ b/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml @@ -0,0 +1,5 @@ +--- +title: Document graphicsmagick installation for source installation +merge_request: 24404 +author: Alexis Reigel +type: added diff --git a/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml b/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml new file mode 100644 index 00000000000..eda69b32094 --- /dev/null +++ b/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml @@ -0,0 +1,5 @@ +--- +title: Show the correct error page when access is denied +merge_request: 23932 +author: +type: fixed diff --git a/changelogs/unreleased/fix-runner-eternal-loop-when-update-job-result.yml b/changelogs/unreleased/fix-runner-eternal-loop-when-update-job-result.yml new file mode 100644 index 00000000000..5a6c36e6f5f --- /dev/null +++ b/changelogs/unreleased/fix-runner-eternal-loop-when-update-job-result.yml @@ -0,0 +1,5 @@ +--- +title: Fix runner eternal loop when update job result +merge_request: 24481 +author: +type: fixed diff --git a/changelogs/unreleased/gitaly-update-1-13-0.yml b/changelogs/unreleased/gitaly-update-1-13-0.yml new file mode 100644 index 00000000000..73de25a532d --- /dev/null +++ b/changelogs/unreleased/gitaly-update-1-13-0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Gitaly to 1.13.0 +merge_request: 24429 +author: +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml b/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml new file mode 100644 index 00000000000..1acea10fcaa --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/projects/project_members` +merge_request: 23227 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml b/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml new file mode 100644 index 00000000000..b612bb3ee39 --- /dev/null +++ b/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml @@ -0,0 +1,5 @@ +--- +title: Remove all `$theme-gray-{weight}` variables in favor of `$gray-{weight}` +merge_request: 24333 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/homepage-proj-descr-cutoff.yml b/changelogs/unreleased/homepage-proj-descr-cutoff.yml new file mode 100644 index 00000000000..837c01f6722 --- /dev/null +++ b/changelogs/unreleased/homepage-proj-descr-cutoff.yml @@ -0,0 +1,5 @@ +--- +title: Increase line height of project summaries +merge_request: +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/move-job-cancel-btn.yml b/changelogs/unreleased/move-job-cancel-btn.yml new file mode 100644 index 00000000000..41f8e1be5f8 --- /dev/null +++ b/changelogs/unreleased/move-job-cancel-btn.yml @@ -0,0 +1,5 @@ +--- +title: Move cancel & new issue button on job page +merge_request: 24074 +author: +type: changed diff --git a/changelogs/unreleased/osw-enforces-project-removal-with-past-failed-attempts.yml b/changelogs/unreleased/osw-enforces-project-removal-with-past-failed-attempts.yml new file mode 100644 index 00000000000..6a2a67e7aa8 --- /dev/null +++ b/changelogs/unreleased/osw-enforces-project-removal-with-past-failed-attempts.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup stale +deleted repo paths on project removal (adjusts project removal bug) +merge_request: 24269 +author: +type: fixed diff --git a/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml b/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml new file mode 100644 index 00000000000..1f80d7535a5 --- /dev/null +++ b/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml @@ -0,0 +1,5 @@ +--- +title: Adjust applied suggestion reverting previous changes +merge_request: 24250 +author: +type: fixed diff --git a/changelogs/unreleased/raise-on-unfiltered-params.yml b/changelogs/unreleased/raise-on-unfiltered-params.yml new file mode 100644 index 00000000000..531e9ba807e --- /dev/null +++ b/changelogs/unreleased/raise-on-unfiltered-params.yml @@ -0,0 +1,5 @@ +--- +title: Actually set raise_on_unfiltered_parameters to true +merge_request: 24443 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml b/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml new file mode 100644 index 00000000000..06546bc5a8e --- /dev/null +++ b/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml @@ -0,0 +1,5 @@ +--- +title: Remove Cancel all jobs button in general jobs list view +merge_request: +author: Jordi Llull +type: removed diff --git a/changelogs/unreleased/security-2770-verify-bundle-import-files.yml b/changelogs/unreleased/security-2770-verify-bundle-import-files.yml new file mode 100644 index 00000000000..dea40dd1ef1 --- /dev/null +++ b/changelogs/unreleased/security-2770-verify-bundle-import-files.yml @@ -0,0 +1,5 @@ +--- +title: Validate bundle files before unpacking them +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml b/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml new file mode 100644 index 00000000000..d1d4412eb50 --- /dev/null +++ b/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml @@ -0,0 +1,5 @@ +--- +title: Fix duplicate project disk path in BackfillLegacyProjectRepositories +merge_request: 24213 +author: +type: changed diff --git a/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml b/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml new file mode 100644 index 00000000000..87405fa0a78 --- /dev/null +++ b/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml @@ -0,0 +1,5 @@ +--- +title: Fix Bitbucket Server importer error handling +merge_request: 24343 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-real-size-warnings.yml b/changelogs/unreleased/sh-fix-real-size-warnings.yml deleted file mode 100644 index 5062ffd677c..00000000000 --- a/changelogs/unreleased/sh-fix-real-size-warnings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix broken templated "Too many changes to show" text -merge_request: 24282 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-request-profiles-html.yml b/changelogs/unreleased/sh-fix-request-profiles-html.yml deleted file mode 100644 index 74e4115db8e..00000000000 --- a/changelogs/unreleased/sh-fix-request-profiles-html.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix requests profiler in admin page not rendering HTML properly -merge_request: 24291 -author: -type: fixed diff --git a/changelogs/unreleased/sh-preload-associations-for-group-api.yml b/changelogs/unreleased/sh-preload-associations-for-group-api.yml new file mode 100644 index 00000000000..24e424b7efb --- /dev/null +++ b/changelogs/unreleased/sh-preload-associations-for-group-api.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate N+1 queries in /api/groups/:id +merge_request: 24513 +author: +type: performance diff --git a/changelogs/unreleased/shared_with_group_path.yml b/changelogs/unreleased/shared_with_group_path.yml new file mode 100644 index 00000000000..73ba9a9f30a --- /dev/null +++ b/changelogs/unreleased/shared_with_group_path.yml @@ -0,0 +1,5 @@ +--- +title: Add group full path to project's shared_with_groups +merge_request: 24052 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/suggestion-dashes.yml b/changelogs/unreleased/suggestion-dashes.yml deleted file mode 100644 index e99ab30b263..00000000000 --- a/changelogs/unreleased/suggestion-dashes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed diff suggestions removing dashes -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml b/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml new file mode 100644 index 00000000000..1af1fe09f33 --- /dev/null +++ b/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml @@ -0,0 +1,5 @@ +--- +title: 'i18n: externalize strings from ''app/views/search''' +merge_request: 24297 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/update-gitlab-styles.yml b/changelogs/unreleased/update-gitlab-styles.yml new file mode 100644 index 00000000000..379f0ad4486 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-styles.yml @@ -0,0 +1,5 @@ +--- +title: Update gitlab-styles to 2.5.1 +merge_request: 24336 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/update-sidekiq-cron.yml b/changelogs/unreleased/update-sidekiq-cron.yml new file mode 100644 index 00000000000..edce32e3753 --- /dev/null +++ b/changelogs/unreleased/update-sidekiq-cron.yml @@ -0,0 +1,6 @@ +--- +title: Update sidekiq-cron to 1.0.4 and use fugit to replace rufus-scheduler to parse + cron syntax +merge_request: 24235 +author: +type: other diff --git a/changelogs/unreleased/zj-feature-gate-set-project-path.yml b/changelogs/unreleased/zj-feature-gate-set-project-path.yml new file mode 100644 index 00000000000..b426a2f3fe7 --- /dev/null +++ b/changelogs/unreleased/zj-feature-gate-set-project-path.yml @@ -0,0 +1,5 @@ +--- +title: Allow setting of feature gates per project +merge_request: 24184 +author: +type: added diff --git a/config/application.rb b/config/application.rb index 349c7258852..92a3d031c63 100644 --- a/config/application.rb +++ b/config/application.rb @@ -162,6 +162,9 @@ module Gitlab config.action_view.sanitized_allowed_protocols = %w(smb) + # Can be removed once upgraded to Rails 5.1 or higher + config.action_controller.raise_on_unfiltered_parameters = true + # Nokogiri is significantly faster and uses less memory than REXML ActiveSupport::XmlMini.backend = 'Nokogiri' diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb index a1e0667bc6f..115ee08dbb6 100644 --- a/config/initializers/new_framework_defaults.rb +++ b/config/initializers/new_framework_defaults.rb @@ -8,8 +8,6 @@ # # Read the Guide for Upgrading Ruby on Rails for more info on each option. -Rails.application.config.action_controller.raise_on_unfiltered_parameters = true - # Enable per-form CSRF tokens. Previous versions had false. Rails.application.config.action_controller.per_form_csrf_tokens = false diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 2a6c5148f71..abc91c3ae51 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -19,7 +19,7 @@ def configure_sentry config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) # Sanitize authentication headers config.sanitize_http_headers = %w[Authorization Private-Token] - config.tags = { program: Gitlab::Sentry.program_context } + config.tags = { program: Gitlab.process_name } end end end diff --git a/config/initializers/tracing.rb b/config/initializers/tracing.rb new file mode 100644 index 00000000000..be95f30d075 --- /dev/null +++ b/config/initializers/tracing.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +if Gitlab::Tracing.enabled? + require 'opentracing' + + # In multi-processed clustered architectures (puma, unicorn) don't + # start tracing until the worker processes are spawned. This works + # around issues when the opentracing implementation spawns threads + Gitlab::Cluster::LifecycleEvents.on_worker_start do + tracer = Gitlab::Tracing::Factory.create_tracer(Gitlab.process_name, Gitlab::Tracing.connection_string) + OpenTracing.global_tracer = tracer if tracer + end +end diff --git a/config/jsdocs.config.js b/config/jsdocs.config.js new file mode 100644 index 00000000000..52635b1ce13 --- /dev/null +++ b/config/jsdocs.config.js @@ -0,0 +1,14 @@ +module.exports = { + source: { + include: ['app/assets/javascripts/'], + }, + opts: { + template: 'node_modules/docdash', + destination: 'jsdoc/', + recurse: true, + }, + docdash: { + search: true, + static: true, + }, +}; diff --git a/config/routes/project.rb b/config/routes/project.rb index 797bf6de37b..d9afb4e7bc8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -256,8 +256,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do collection do - post :cancel_all - resources :artifacts, only: [] do collection do get :latest_succeeded, diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 8bdc7c6556c..2051bcff8f0 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -27,6 +27,9 @@ Gitlab::Seeder.quiet do Sidekiq::Worker.skipping_transaction_check do MergeRequests::CreateService.new(project, developer, params).execute + rescue Repository::AmbiguousRefError + # Ignore pipelines creation errors for now, we can doing that after + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55966. will be resolved. end print '.' end diff --git a/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb b/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb new file mode 100644 index 00000000000..11659846a06 --- /dev/null +++ b/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CleanupLegacyArtifactMigration < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class Build < ActiveRecord::Base + include EachBatch + + self.table_name = 'ci_builds' + self.inheritance_column = :_type_disabled + + scope :with_legacy_artifacts, -> { where("artifacts_file <> ''") } + end + + def up + Gitlab::BackgroundMigration.steal('MigrateLegacyArtifacts') + + CleanupLegacyArtifactMigration::Build + .with_legacy_artifacts + .each_batch(of: 100) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::MigrateLegacyArtifacts.new.perform(*range) + end + end + + def down + # no-op + end +end diff --git a/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb b/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb new file mode 100644 index 00000000000..073faf721ae --- /dev/null +++ b/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RemovePartialIndexFromCiBuildsArtifactsFile < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'partial_index_ci_builds_on_id_with_legacy_artifacts'.freeze + + disable_ddl_transaction! + + def up + remove_concurrent_index_by_name(:ci_builds, INDEX_NAME) + end + + def down + add_concurrent_index(:ci_builds, :id, where: "artifacts_file <> ''", name: INDEX_NAME) + end +end diff --git a/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb b/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb new file mode 100644 index 00000000000..d8f979a1848 --- /dev/null +++ b/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddErrorNotificationSentToRemoteMirrors < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :remote_mirrors, :error_notification_sent, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 87826881d58..c6fef9b5d11 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190103140724) do +ActiveRecord::Schema.define(version: 20190115054216) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -354,7 +354,6 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.index ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree t.index ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree t.index ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree - t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree @@ -1849,6 +1848,7 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.string "encrypted_credentials_salt" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "error_notification_sent" t.index ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree t.index ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree end diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index cf37eaa0b61..05c1923f0cb 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -217,6 +217,8 @@ repository from your GitLab server over HTTP. ## TLS support +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22602) in GitLab 11.7. + Gitaly supports TLS credentials for GRPC authentication. To be able to communicate with a gitaly instance that listens for secure connections you will need to use `tls://` url scheme in the `gitaly_address` of the corresponding storage entry in the gitlab configuration. diff --git a/doc/api/avatar.md b/doc/api/avatar.md index aa6f7c185ae..e55fffba4b2 100644 --- a/doc/api/avatar.md +++ b/doc/api/avatar.md @@ -1,33 +1,41 @@ # Avatar API -> [Introduced][ce-19121] in GitLab 11.0 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121) in GitLab 11.0. ## Get a single avatar URL -Get a single avatar URL for a given email address. If user with matching public -email address is not found, results from external avatar services are returned. -This endpoint can be accessed without authentication. In case public visibility -is restricted, response will be `403 Forbidden` when unauthenticated. +Get a single [avatar](../user/profile/index.md#profile-settings) URL for a user with the given email address. -``` +If: + +- No user with the given public email address is found, results from external avatar services are + returned. +- Public visibility is restricted, response will be `403 Forbidden` when unauthenticated. + +NOTE: **Note:** +This endpoint can be accessed without authentication. + +```text GET /avatar?email=admin@example.com ``` -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | --------------------- | -| `email` | string | yes | Public email address of the user | -| `size` | integer | no | Single pixel dimension (since images are squares). Only used for avatar lookups at `Gravatar` or at the configured `Libravatar` server | +Parameters: -```bash -curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `email` | string | yes | Public email address of the user. | +| `size` | integer | no | Single pixel dimension (since images are squares). Only used for avatar lookups at `Gravatar` or at the configured `Libravatar` server. | + +Example request: + +```sh +curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com&size=32 ``` Example response: ```json { - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=64&d=identicon" } ``` - -[ce-19121]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121 diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 41e39c31069..92936a277ac 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -1,19 +1,26 @@ # Award Emoji API -> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575) in GitLab 8.9. Snippet support added in 8.12. +An [awarded emoji](../user/award_emojis.md) tells a thousand words. -An awarded emoji tells a thousand words, and can be awarded on issues, merge -requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called -`awardables`. +Emoji can be awarded on the following (known as "awardables"): + +- [Issues](../user/project/issues/index.md) ([API](issues.md)). +- [Merge requests](../user/project/merge_requests/index.md) ([API](merge_requests.md)). +- [Snippets](../user/snippets.md) ([API](snippets.md)). + +Emoji can also [be awarded](../user/award_emojis.html#award-emoji-for-comments) on comments (also known as notes). See also [Notes API](notes.md). ## Issues, merge requests, and snippets +See [Award Emoji on Comments](#award-emoji-on-comments) for information on using these endpoints with comments. + ### List an awardable's award emoji -Gets a list of all award emoji +Get a list of all award emoji for a specified awardable. -``` +```text GET /projects/:id/issues/:issue_iid/award_emoji GET /projects/:id/merge_requests/:merge_request_iid/award_emoji GET /projects/:id/snippets/:snippet_id/award_emoji @@ -21,16 +28,18 @@ GET /projects/:id/snippets/:snippet_id/award_emoji 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 | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji ``` -Example Response: +Example response: ```json [ @@ -71,9 +80,9 @@ Example Response: ### Get single award emoji -Gets a single award emoji from an issue, snippet, or merge request. +Get a single award emoji from an issue, snippet, or merge request. -``` +```text GET /projects/:id/issues/:issue_iid/award_emoji/:award_id GET /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id @@ -81,17 +90,19 @@ GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id 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 | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `award_id` | integer | yes | ID of the award emoji. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/1 ``` -Example Response: +Example response: ```json { @@ -114,9 +125,9 @@ Example Response: ### Award a new emoji -This end point creates an award emoji on the specified resource +Create an award emoji on the specified awardable. -``` +```text POST /projects/:id/issues/:issue_iid/award_emoji POST /projects/:id/merge_requests/:merge_request_iid/award_emoji POST /projects/:id/snippets/:snippet_id/award_emoji @@ -124,13 +135,13 @@ POST /projects/:id/snippets/:snippet_id/award_emoji 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 | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `name` | string | yes | Name of the emoji without colons. | -```bash +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji?name=blowfish ``` @@ -157,10 +168,12 @@ Example Response: ### Delete an award emoji -Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. +Sometimes it's just not meant to be and you'll have to remove the award. -``` +NOTE: **Note:** +Only available to administrators or the author of the award. + +```text DELETE /projects/:id/issues/:issue_iid/award_emoji/:award_id DELETE /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id @@ -168,43 +181,47 @@ DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id 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 | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `award_id` | integer | yes | The ID of an award_emoji | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `award_id` | integer | yes | ID of an award emoji. | -```bash +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344 ``` -## Award Emoji on Notes +## Award Emoji on Comments -The endpoints documented above are available for Notes as well. Notes -are a sub-resource of Issues, Merge Requests, or Snippets. The examples below -describe working with Award Emoji on notes for an Issue, but can be -easily adapted for notes on a Merge Request. +Comments (also known as notes) are a sub-resource of issues, merge requests, and snippets. -### List a note's award emoji +NOTE: **Note:** +The examples below describe working with award emoji on comments for an issue, but can be +easily adapted for comments on a merge request. -``` +### List a comment's award emoji + +Get all award emoji for a comment (note). + +```text GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` 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 | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji ``` -Example Response: +Example response: ```json [ @@ -227,26 +244,30 @@ Example Response: ] ``` -### Get single note's award emoji +### Get an award emoji for a comment -``` +Get a single award emoji for a comment (note). + +```text GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` 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 | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `award_id` | integer | yes | ID of the award emoji. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji/2 ``` -Example Response: +Example response: ```json { @@ -267,26 +288,30 @@ Example Response: } ``` -### Award a new emoji on a note +### Award a new emoji on a comment -``` +Create an award emoji on the specified comment (note). + +```text POST /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` 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 | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `name` | string | yes | Name of the emoji without colons. | -```bash +Example request: + +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji?name=rocket ``` -Example Response: +Example response: ```json { @@ -307,26 +332,28 @@ Example Response: } ``` -### Delete an award emoji +### Delete an award emoji from a comment -Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. +Sometimes it's just not meant to be and you'll have to remove the award. -``` +NOTE: **Note:** +Only available to administrators or the author of the award. + +```text DELETE /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` 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 | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of an award_emoji | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `award_id` | integer | yes | ID of an award_emoji. | + +Example request: -```bash +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345 ``` - -[ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575 diff --git a/doc/api/branches.md b/doc/api/branches.md index 3b55154887d..8d5f333ba77 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -1,21 +1,31 @@ # Branches API +This API operates on [repository branches](../user/project/repository/branches/index.md). + +TIP: **Tip:** +See also [Protected branches API](protected_branches.md). + ## List repository branches Get a list of repository branches from a project, sorted by name alphabetically. -This endpoint can be accessed without authentication if the repository is -publicly accessible. -``` +NOTE: **Note:** +This endpoint can be accessed without authentication if the repository is publicly accessible. + +```text GET /projects/:id/repository/branches ``` -| 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 | -| `search` | string | no | Return list of branches matching the search criteria. | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `search` | string | no | Return list of branches matching the search criteria. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches ``` @@ -53,19 +63,25 @@ Example response: ## Get single repository branch -Get a single project repository branch. This endpoint can be accessed without -authentication if the repository is publicly accessible. +Get a single project repository branch. -``` +NOTE: **Note:** +This endpoint can be accessed without authentication if the repository is publicly accessible. + +```text GET /projects/:id/repository/branches/:branch ``` -| 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 | -| `branch` | string | yes | The name of the branch | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master ``` @@ -100,120 +116,34 @@ Example response: ## Protect repository branch ->**Note:** This API endpoint is deprecated in favor of `POST /projects/:id/protected_branches`. - -Protects a single project repository branch. This is an idempotent function, -protecting an already protected repository branch still returns a `200 OK` -status code. - -``` -PUT /projects/:id/repository/branches/:branch/protect -``` - -```bash -curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true -``` - -| 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 | -| `branch` | string | yes | The name of the branch | -| `developers_can_push` | boolean | no | Flag if developers can push to the branch | -| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch | - -Example response: - -```json -{ - "commit": { - "author_email": "john@example.com", - "author_name": "John Smith", - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00", - "committer_email": "john@example.com", - "committer_name": "John Smith", - "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "short_id": "7b5c3cc", - "title": "add projects API", - "message": "add projects API", - "parent_ids": [ - "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - ] - }, - "name": "master", - "merged": false, - "protected": true, - "default": true, - "developers_can_push": true, - "developers_can_merge": true, - "can_push": true -} -``` +See [`POST /projects/:id/protected_branches`](protected_branches.md#protect-repository-branches) for +information on protecting repository branches. ## Unprotect repository branch ->**Note:** This API endpoint is deprecated in favor of `DELETE /projects/:id/protected_branches/:name` +See [`DELETE /projects/:id/protected_branches/:name`](protected_branches.md#unprotect-repository-branches) +for information on unprotecting repository branches. -Unprotects a single project repository branch. This is an idempotent function, -unprotecting an already unprotected repository branch still returns a `200 OK` -status code. - -``` -PUT /projects/:id/repository/branches/:branch/unprotect -``` - -```bash -curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/unprotect -``` - -| 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 | -| `branch` | string | yes | The name of the branch | +## Create repository branch -Example response: +Create a new branch in the repository. -```json -{ - "commit": { - "author_email": "john@example.com", - "author_name": "John Smith", - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00", - "committer_email": "john@example.com", - "committer_name": "John Smith", - "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "short_id": "7b5c3cc", - "title": "add projects API", - "message": "add projects API", - "parent_ids": [ - "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - ] - }, - "name": "master", - "merged": false, - "protected": false, - "default": true, - "developers_can_push": false, - "developers_can_merge": false, - "can_push": true -} +```text +POST /projects/:id/repository/branches ``` -## Create repository branch +Parameters: -``` -POST /projects/:id/repository/branches -``` +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | +| `ref` | string | yes | Branch name or commit SHA to create branch from. | -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `branch` | string | yes | The name of the branch | -| `ref` | string | yes | The branch name or commit SHA to create branch from | +Example request: -```bash -curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master" +```sh +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master ``` Example response: @@ -247,36 +177,47 @@ Example response: ## Delete repository branch -``` +Delete a branch from the repository. + +NOTE: **Note:** +In the case of an error, an explanation message is provided. + +```text DELETE /projects/:id/repository/branches/:branch ``` -| 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 | -| `branch` | string | yes | The name of the branch | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | -In case of an error, an explaining message is provided. +Example request: -```bash -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch" +```sh +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch ``` ## Delete merged branches Will delete all branches that are merged into the project's default branch. -Protected branches will not be deleted as part of this operation. +NOTE: **Note:** +[Protected branches](../user/project/protected_branches.md) will not be deleted as part of this operation. -``` +```text DELETE /projects/:id/repository/merged_branches ``` -| 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 | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +Example request: -```bash -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches" +```sh +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/merged_branches ``` diff --git a/doc/api/features.md b/doc/api/features.md index 59f1005ef72..47f104e1f20 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -60,9 +60,10 @@ POST /features/:name | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | | `feature_group` | string | no | A Feature group name | | `user` | string | no | A GitLab username | +| `project` | string | no | A projects path, for example 'gitlab-org/gitlab-ce' | -Note that you can enable or disable a feature for both a `feature_group` and a -`user` with a single API call. +Note that you can enable or disable a feature for a `feature_group`, a `user`, +and a `project` in a single API call. ```bash curl --data "value=30" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/new_library diff --git a/doc/api/groups.md b/doc/api/groups.md index 2d9114c40ea..907b443d355 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -357,12 +357,14 @@ Example response: { "group_id": 4, "group_name": "Twitter", + "group_full_path": "twitter", "group_access_level": 30, "expires_at": null }, { "group_id": 3, "group_name": "Gitlab Org", + "group_full_path": "gitlab-org", "group_access_level": 10, "expires_at": "2018-08-14" } diff --git a/doc/api/import.md b/doc/api/import.md new file mode 100644 index 00000000000..9f8e0d232c6 --- /dev/null +++ b/doc/api/import.md @@ -0,0 +1,33 @@ +# Import API + +## Import repository from GitHub + +Import your projects from GitHub to GitLab via the API. + +``` +POST /import/github +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `personal_access_token` | string | yes | GitHub personal access token | +| `repo_id` | integer | yes | GitHub repository ID | +| `new_name` | string | no | New repo name | +| `target_namespace` | string | yes | Namespace to import repo into | + + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "personal_access_token=abc123&repo_id=12345&target_namespace=root" https://gitlab.example.com/api/v4/import/github +``` + +Example response: + +```json +{ + "id": 27, + "name": "my-repo", + "full_path": "/root/my-repo", + "full_name": "Administrator / my-repo" +} +``` + diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 6786c0c5b5c..6e156a14b25 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -1,126 +1,165 @@ # GitLab as an OAuth2 provider -This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow other services access GitLab resources on user's behalf. +This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow +other services to access GitLab resources on user's behalf. -If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [OAuth2 provider](../integration/oauth_provider.md) -documentation. +If you want GitLab to be an OAuth authentication service provider to sign into +other services, see the [OAuth2 provider](../integration/oauth_provider.md) +documentation. This functionality is based on the +[doorkeeper Ruby gem](https://github.com/doorkeeper-gem/doorkeeper). -This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper). +## Supported OAuth2 flows -## Supported OAuth2 Flows +GitLab currently supports the following authorization flows: -GitLab currently supports following authorization flows: +- **Web application flow:** Most secure and common type of flow, designed for + applications with secure server-side. +- **Implicit grant flow:** This flow is designed for user-agent only apps (e.g., single + page web application running on GitLab Pages). +- **Resource owner password credentials flow:** To be used **only** for securely + hosted, first-party services. -- *Web Application Flow* - Most secure and common type of flow, designed for the applications with secure server-side. -- *Implicit Flow* - This flow is designed for user-agent only apps (e.g. single page web application running on GitLab Pages). -- *Resource Owner Password Credentials Flow* - To be used **only** for securely hosted, first-party services. +Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out +how all those flows work and pick the right one for your use case. -Please refer to [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out in details how all those flows work and pick the right one for your use case. +Both **web application** and **implicit grant** flows require `application` to be +registered first via the `/profile/applications` page in your user's account. +During registration, by enabling proper scopes, you can limit the range of +resources which the `application` can access. Upon creation, you'll obtain the +`application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**. -Both *web application* and *implicit* flows require `application` to be registered first via `/profile/applications` page -in your user's account. During registration, by enabling proper scopes you can limit the range of resources which the `application` can access. Upon creation -you'll obtain `application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**. +CAUTION: **Important:** +OAuth specification advises sending the `state` parameter with each request to +`/oauth/authorize`. We highly recommended sending a unique value with each request +and validate it against the one in the redirect request. This is important in +order to prevent [CSRF attacks](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)). +The `state` parameter really should have been a requirement in the standard! ->**Important:** OAuth specification advises sending `state` parameter with each request to `/oauth/authorize`. We highly recommended to send a unique -value with each request and validate it against the one in redirect request. This is important to prevent [CSRF attacks]. The `state` param really should -have been a requirement in the standard! +In the following sections you will find detailed instructions on how to obtain +authorization with each flow. -In the following sections you will find detailed instructions on how to obtain authorization with each flow. +### Web application flow -### Web Application Flow +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.1) for a +detailed flow description. -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1) for a detailed flow description +The web application flow is: -#### 1. Requesting authorization code +1. Request authorization code. To do that, you should redirect the user to the + `/oauth/authorize` endpoint with the following GET parameters: -To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint with following GET parameters: + ``` + https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH + ``` -``` -https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH -``` + This will ask the user to approve the applications access to their account and + then redirect back to the `REDIRECT_URI` you provided. The redirect will + include the GET `code` parameter, for example: -This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect will -include the GET `code` parameter, for example: + ``` + http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH + ``` -`http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH` + You should then use `code` to request an access token. -You should then use the `code` to request an access token. +1. Once you have the authorization code you can request an `access_token` using the + code. You can do that by using any HTTP client. In the following example, + we are using Ruby's `rest-client`: -#### 2. Requesting access token + ```ruby + parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' + RestClient.post 'http://gitlab.example.com/oauth/token', parameters + ``` -Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, -we are using Ruby's `rest-client`: + Example response: -``` -parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' -RestClient.post 'http://gitlab.example.com/oauth/token', parameters + ```json + { + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" + } + ``` -# The response will be -{ - "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", - "token_type": "bearer", - "expires_in": 7200, - "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" -} -``` ->**Note:** -The `redirect_uri` must match the `redirect_uri` used in the original authorization request. +NOTE: **Note:** +The `redirect_uri` must match the `redirect_uri` used in the original +authorization request. You can now make requests to the API with the access token returned. +### Implicit grant flow -### Implicit Grant - -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.2) for a detailed flow description. - -Unlike the web flow, the client receives an `access token` immediately as a result of the authorization request. The flow does not use client secret -or authorization code because all of the application code and storage is easily accessible, therefore __secrets__ can leak easily. +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.2) for a +detailed flow description. ->**Important:** Avoid using this flow for applications that store data outside of the GitLab instance. If you do, make sure to verify `application id` -associated with access token before granting access to the data +CAUTION: **Important:** +Avoid using this flow for applications that store data outside of the GitLab +instance. If you do, make sure to verify `application id` associated with the +access token before granting access to the data (see [/oauth/token/info](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo)). +Unlike the web flow, the client receives an `access token` immediately as a +result of the authorization request. The flow does not use the client secret +or the authorization code because all of the application code and storage is +easily accessible, therefore secrets can leak easily. -#### 1. Requesting access token - -To request the access token, you should redirect the user to the `/oauth/authorize` endpoint using `token` response type: +To request the access token, you should redirect the user to the +`/oauth/authorize` endpoint using `token` response type: ``` https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=token&state=YOUR_UNIQUE_STATE_HASH ``` -This will ask the user to approve the application's access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect -will include a fragment with `access_token` as well as token details in GET parameters, for example: +This will ask the user to approve the application's access to their account and +then redirect them back to the `REDIRECT_URI` you provided. The redirect +will include a fragment with `access_token` as well as token details in GET +parameters, for example: ``` http://myapp.com/oauth/redirect#access_token=ABCDExyz123&state=YOUR_UNIQUE_STATE_HASH&token_type=bearer&expires_in=3600 ``` -### Resource Owner Password Credentials +### Resource owner password credentials flow -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.3) for a detailed flow description. +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.3) for a +detailed flow description. -> **Deprecation notice:** Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication -turned on. These users can access the API using [personal access tokens] instead. +NOTE: **Note:** +The Resource Owner Password Credentials is disabled for users with [two-factor +authentication](../user/profile/account/two_factor_authentication.md) turned on. +These users can access the API using [personal access tokens](../user/profile/personal_access_tokens.md) +instead. -In this flow, a token is requested in exchange for the resource owner credentials (username and password). -The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the -client is part of the device operating system or a highly privileged application), and when other authorization grant types are not -available (such as an authorization code). +In this flow, a token is requested in exchange for the resource owner credentials +(username and password). ->**Important:** -Never store the user's credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] -are a better choice. +The credentials should only be used when: -Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used -for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the -resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token. +- There is a high degree of trust between the resource owner and the client. For + example, the client is part of the device operating system or a highly + privileged application. +- Other authorization grant types are not available (such as an authorization code). -#### 1. Requesting access token +CAUTION: **Important:** +Never store the user's credentials and only use this grant type when your client +is deployed to a trusted environment, in 99% of cases +[personal access tokens](../user/profile/personal_access_tokens.md) are a better +choice. -POST request to `/oauth/token` with parameters: +Even though this grant type requires direct client access to the resource owner +credentials, the resource owner credentials are used for a single request and +are exchanged for an access token. This grant type can eliminate the need for +the client to store the resource owner credentials for future use, by exchanging +the credentials with a long-lived access token or refresh token. -``` +To request an access token, you must make a POST request to `/oauth/token` with +the following parameters: + +```json { "grant_type" : "password", "username" : "user@example.com", @@ -128,6 +167,13 @@ POST request to `/oauth/token` with parameters: } ``` +Example cURL request: + +```sh +echo 'grant_type=password&username=<your_username>&password=<your_password>' > auth.txt +curl --data "@auth.txt" --request POST https://gitlab.example.com/oauth/token +``` + Then, you'll receive the access token back in the response: ``` @@ -138,7 +184,7 @@ Then, you'll receive the access token back in the response: } ``` -For testing you can use the oauth2 ruby gem: +For testing, you can use the `oauth2` Ruby gem: ``` client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com") @@ -148,7 +194,9 @@ puts access_token.token ## Access GitLab API with `access token` -The `access token` allows you to make requests to the API on a behalf of a user. You can pass the token either as GET parameter +The `access token` allows you to make requests to the API on behalf of a user. +You can pass the token either as GET parameter: + ``` GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN ``` @@ -159,5 +207,3 @@ or you can put the token to the Authorization header: curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user ``` -[personal access tokens]: ../user/profile/personal_access_tokens.md -[CSRF attacks]: http://www.oauthsecurity.com/#user-content-authorization-code-flow diff --git a/doc/api/projects.md b/doc/api/projects.md index 538cd34de43..1296b435792 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -525,11 +525,13 @@ GET /projects/:id { "group_id": 4, "group_name": "Twitter", + "group_full_path": "twitter", "group_access_level": 30 }, { "group_id": 3, "group_name": "Gitlab Org", + "group_full_path": "gitlab-org", "group_access_level": 10 } ], diff --git a/doc/api/tags.md b/doc/api/tags.md index fc86aaa6757..23dbf2d9ff7 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -17,6 +17,9 @@ Parameters: | `id` | integer/string| yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user| | `order_by` | string | no | Return tags ordered by `name` or `updated` fields. Default is `updated` | | `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Return list of tags matching the search criteria | + +> Support for `search` was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/54401) in GitLab 11.8. ```json [ diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index fef367051bf..cd1a6695fbd 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -521,11 +521,11 @@ stages: variables: DOCKER_HOST: tcp://docker:2375 DOCKER_DRIVER: overlay2 - CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_SLUG - CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest + CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest before_script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY build: stage: build diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 010c579b83e..b9b5ceab7fb 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -416,81 +416,15 @@ and/or `production`) you can see this information in the merge request itself. ### Go directly from source files to public pages on the environment -> Introduced in GitLab 8.17. In GitLab 11.5 the file links -are surfaced to the merge request widget. - -You can specify a Route Map to get GitLab to show **View on ...** -buttons to go directly from a file to that file's representation on the -[deployed website via Review Apps](review_apps/index.md). - -To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map. - -A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website). -Below is an example of a route map for [Middleman](https://middlemanapp.com) static websites -like <https://gitlab.com/gitlab-com/www-gitlab-com>: - -```yaml -# Team data -- source: 'data/team.yml' # data/team.yml - public: 'team/' # team/ - -# Blogposts -- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb - public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ - -# HTML files -- source: /source\/(.+?\.html).*/ # source/index.html.haml - public: '\1' # index.html - -# Other files -- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png - public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png -``` - -Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys: - -- `source` - - a string, starting and ending with `'`, for an exact match - - a regular expression, starting and ending with `/`, for a pattern match - - The regular expression needs to match the entire source path - `^` and `$` anchors are implied. - - Can include capture groups denoted by `()` that can be referred to in the `public` path. - - Slashes (`/`) can, but don't have to, be escaped as `\/`. - - Literal periods (`.`) should be escaped as `\.`. -- `public` - - a string, starting and ending with `'`. - - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurrence, starting with `\1`. - -The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate. - -In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`. - ---- - -Once you have the route mapping set up, it will be exposed in a few places: - -- In the merge request widget. The **View app** button will take you to the - environment URL you have set up in `.gitlab-ci.yml`. The dropdown will render - the first 5 matched items from the route map, but you can filter them if more - than 5 are available. - - ![View app file list in merge request widget](img/view_on_mr_widget.png) - -- In the diff for a merge request, comparison, or commit. - - !["View on env" button in merge request diff](img/view_on_env_mr.png) - -- In the blob file view. - - !["View on env" button in file view](img/view_on_env_blob.png) | - ---- - -We now have a full development cycle, where our app is tested, built, deployed -as a Review app, deployed to a staging server once the merge request is merged, -and finally manually deployed to the production server. What we just described -is a single workflow, but imagine tens of developers working on a project -at the same time. They each push to their branches, and dynamic environments are -created all the time. In that case, we probably need to do some clean up. Read +With GitLab's [Route Maps](review_apps/index.md#route-maps) you can go directly +from source files to public pages on the environment set for Review Apps. + +From then on, you have a full development cycle, where your app is tested, built, deployed +as a Review App, deployed to a staging server once the merge request is merged, +and finally manually deployed to the production server. This is a simple workflow, +but when you have multiple developers working on a project +at the same time, each of them pushing to their own branches, dynamic environments are +created all the time. In which case, you probably want to do some clean up. Read next how environments can be stopped. ## Stopping an environment diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/review_apps/img/view_on_env_blob.png Binary files differindex acc457fbb38..acc457fbb38 100644 --- a/doc/ci/img/view_on_env_blob.png +++ b/doc/ci/review_apps/img/view_on_env_blob.png diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/review_apps/img/view_on_env_mr.png Binary files differindex 2c0bd25a4f2..2c0bd25a4f2 100644 --- a/doc/ci/img/view_on_env_mr.png +++ b/doc/ci/review_apps/img/view_on_env_mr.png diff --git a/doc/ci/img/view_on_mr_widget.png b/doc/ci/review_apps/img/view_on_mr_widget.png Binary files differindex efe023b07b5..efe023b07b5 100644 --- a/doc/ci/img/view_on_mr_widget.png +++ b/doc/ci/review_apps/img/view_on_mr_widget.png diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 64be011008e..8b3a7b63e62 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -102,3 +102,88 @@ The following are example projects that use Review Apps with: - [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift). See also the video [Demo: Cloud Native Development with GitLab](https://www.youtube.com/watch?v=jfIyQEwrocw), which includes a Review Apps example. + +## Route Maps + +> Introduced in GitLab 8.17. In GitLab 11.5 the file links +are surfaced to the merge request widget. + +Route Maps allows you to go directly from source files +to public pages on the [environment](../environments.md) defined for +Review Apps. Once set up, the review app link in the merge request +widget can take you directly to the pages changed, making it easier +and faster to preview proposed modifications. + +All you need to do is to tell GitLab how the paths of files +in your repository map to paths of pages on your website using a Route Map. +Once set, GitLab will display **View on ...** buttons, which will take you +to the pages changed directly from merge requests. + +To set up a route map, add a a file inside the repository at `.gitlab/route-map.yml`, +which contains a YAML array that maps `source` paths (in the repository) to `public` +paths (on the website). + +### Route Maps example + +Below there's an example of a route map for [Middleman](https://middlemanapp.com), +a static site generator (SSG) used to build [GitLab's website](https://about.gitlab.com), +deployed from its [project on GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com): + +```yaml +# Team data +- source: 'data/team.yml' # data/team.yml + public: 'team/' # team/ + +# Blogposts +- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb + public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ + +# HTML files +- source: /source\/(.+?\.html).*/ # source/index.html.haml + public: '\1' # index.html + +# Other files +- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png + public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png +``` + +Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys: + +- `source` + - a string, starting and ending with `'`, for an exact match + - a regular expression, starting and ending with `/`, for a pattern match + - The regular expression needs to match the entire source path - `^` and `$` anchors are implied. + - Can include capture groups denoted by `()` that can be referred to in the `public` path. + - Slashes (`/`) can, but don't have to, be escaped as `\/`. + - Literal periods (`.`) should be escaped as `\.`. +- `public` + - a string, starting and ending with `'`. + - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurrence, starting with `\1`. + +The public path for a source path is determined by finding the first +`source` expression that matches it, and returning the corresponding +`public` path, replacing the `\N` expressions with the values of the +`()` capture groups if appropriate. + +In the example above, the fact that mappings are evaluated in order +of their definition is used to ensure that `source/index.html.haml` +will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, +and will result in a public path of `index.html`, instead of +`index.html.haml`. + +Once you have the route mapping set up, it will be exposed in a few places: + +- In the merge request widget. The **View app** button will take you to the + environment URL you have set up in `.gitlab-ci.yml`. The dropdown will render + the first 5 matched items from the route map, but you can filter them if more + than 5 are available. + + ![View app file list in merge request widget](img/view_on_mr_widget.png) + +- In the diff for a merge request, comparison, or commit. + + !["View on env" button in merge request diff](img/view_on_env_mr.png) + +- In the blob file view. + + !["View on env" button in file view](img/view_on_env_blob.png) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d4f0da52e53..fb69d888b94 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -78,118 +78,6 @@ A job is defined by a list of parameters that define the job behavior. | [retry](#retry) | no | Define when and how many times a job can be auto-retried in case of a failure | | [parallel](#parallel) | no | Defines how many instances of a job should be run in parallel | -### `extends` - -> Introduced in GitLab 11.3. - -`extends` defines an entry name that a job that uses `extends` is going to -inherit from. - -It is an alternative to using [YAML anchors](#anchors) and is a little -more flexible and readable: - -```yaml -.tests: - script: rake test - stage: test - only: - refs: - - branches - -rspec: - extends: .tests - script: rake rspec - only: - variables: - - $RSPEC -``` - -In the example above, the `rspec` job inherits from the `.tests` template job. -GitLab will perform a reverse deep merge based on the keys. GitLab will: - -- Merge the `rspec` contents into `.tests` recursively. -- Not merge the values of the keys. - -This results in the following `rspec` job: - -```yaml -rspec: - script: rake rspec - stage: test - only: - refs: - - branches - variables: - - $RSPEC -``` - -NOTE: **Note:** -Note that `script: rake test` has been overwritten by `script: rake rspec`. - -If you do want to include the `rake test`, have a look at [before_script-and-after_script](#before_script-and-after_script). - -`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it's -possible to inherit from regular jobs as well. - -`extends` supports multi-level inheritance, however it is not recommended to -use more than three levels. The maximum nesting level that is supported is 10. -The following example has two levels of inheritance: - -```yaml -.tests: - only: - - pushes - -.rspec: - extends: .tests - script: rake rspec - -rspec 1: - variables: - RSPEC_SUITE: '1' - extends: .rspec - -rspec 2: - variables: - RSPEC_SUITE: '2' - extends: .rspec - -spinach: - extends: .tests - script: rake spinach -``` - -`extends` works across configuration files combined with [`include`](#include). - -### `pages` - -`pages` is a special job that is used to upload static content to GitLab that -can be used to serve your website. It has a special syntax, so the two -requirements below must be met: - -1. Any static content must be placed under a `public/` directory -1. `artifacts` with a path to the `public/` directory must be defined - -The example below simply moves all files from the root of the project to the -`public/` directory. The `.public` workaround is so `cp` doesn't also copy -`public/` to itself in an infinite loop: - -```yaml -pages: - stage: deploy - script: - - mkdir .public - - cp -r * .public - - mv .public public - artifacts: - paths: - - public - only: - - master -``` - -Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). - ## `image` and `services` This allows to specify a custom Docker image and a list of services that can be @@ -260,7 +148,7 @@ There are also two edge cases worth mentioning: 1. If no `stages` are defined in `.gitlab-ci.yml`, then the `build`, `test` and `deploy` are allowed to be used as job's stage by default. -2. If a job doesn't specify a `stage`, the job is assigned the `test` stage. +1. If a job doesn't specify a `stage`, the job is assigned the `test` stage. ## `stage` @@ -328,7 +216,7 @@ a "key: value" pair. Be careful when using special characters: jobs are created: 1. `only` defines the names of branches and tags for which the job will run. -2. `except` defines the names of branches and tags for which the job will +1. `except` defines the names of branches and tags for which the job will **not** run. There are a few rules that apply to the usage of job policy: @@ -677,9 +565,9 @@ cleanup_job: The above script will: 1. Execute `cleanup_build_job` only when `build_job` fails. -2. Always execute `cleanup_job` as the last step in pipeline regardless of +1. Always execute `cleanup_job` as the last step in pipeline regardless of success or failure. -3. Allow you to manually execute `deploy_job` from GitLab's UI. +1. Allow you to manually execute `deploy_job` from GitLab's UI. ### `when:manual` @@ -1622,7 +1510,6 @@ Possible values for `when` are: - `missing_dependency_failure`: Retry if a dependency was missing. - `runner_unsupported`: Retry if the runner was unsupported. - ## `parallel` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5. @@ -1645,193 +1532,213 @@ test: ## `include` -> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. -> Available for Starter, Premium and Ultimate since 10.6. -> Behaviour expanded in GitLab 10.8 to allow more flexible overriding. -> [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603) -to GitLab Core in 11.4 -> In GitLab 11.7, support for [including GitLab-supplied templates directly](https://gitlab.com/gitlab-org/gitlab-ce/issues/53445) and support for [including templates from another repository](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) was added. +> - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. +> - Available for Starter, Premium and Ultimate since 10.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603) to GitLab Core in 11.4. Using the `include` keyword, you can allow the inclusion of external YAML files. +`include` requires the external YAML file to have the extensions `.yml` or `.yaml`, +otherwise the external file will not be included. -In the following example, the content of `.before-script-template.yml` will be -automatically fetched and evaluated along with the content of `.gitlab-ci.yml`: +The files defined in `include` are: -```yaml -# Content of https://gitlab.com/awesome-project/raw/master/.before-script-template.yml +- Deep merged with those in `.gitlab-ci.yml`. +- Always evaluated first and merged with the content of `.gitlab-ci.yml`, + regardless of the position of the `include` keyword. -before_script: - - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs - - gem install bundler --no-document - - bundle install --jobs $(nproc) "${FLAGS[@]}" -``` +TIP: **Tip:** +Use merging to customize and override included CI/CD configurations with local +definitions. -```yaml -# Content of .gitlab-ci.yml +Recursive includes are not supported. Your external files should not use the +`include` keyword as it will be ignored. -include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +NOTE: **Note:** +Using YAML aliases across different YAML files sourced by `include` is not +supported. You must only refer to aliases in the same file. Instead +of using YAML anchors, you can use the [`extends` keyword](#extends). -rspec: - script: - - bundle exec rspec -``` +`include` supports four include methods: -NOTE: **Note:** -`include` requires the external YAML files to have the extensions `.yml` or `.yaml`. -The external file will not be included if the extension is missing. +- [`local`](#includelocal) +- [`file`](#includefile) +- [`template`](#includetemplate) +- [`remote`](#includeremote) -You can include your extra YAML file either as a single string or -as an array of multiple values. You can also use full paths or -relative URLs. The following examples are both valid: +See [usage examples](#include-examples). -```yaml -# Single string +### `include:local` -include: '/templates/.after-script-template.yml' -``` +`include:local` includes a file from the same repository as `.gitlab-ci.yml`. +It's referenced using full paths relative to the root directory (`/`). -```yaml -# Single string +You can only use files that are currently tracked by Git on the same branch +your configuration file is on. In other words, when using a `include:local`, make +sure that both `.gitlab-ci.yml` and the local file are on the same branch. +NOTE: **Note:** +Including local files through Git submodules paths is not supported. + +Example: + +```yaml include: - file: '/templates/.after-script-template.yml' + - local: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array +### `include:file` +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) in GitLab 11.7. + +To include files from another private project under the same GitLab instance, +use `include:file`. This file is referenced using full paths relative to the +root directory (`/`). For example: + +```yaml include: - - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - '/templates/.after-script-template.yml' + - project: 'my-group/my-project' + file: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array mixed syntax +You can also specify `ref`, with the default being the `HEAD` of the project: +```yaml include: - - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - '/templates/.after-script-template.yml' - - template: Auto-DevOps.gitlab-ci.yml + - project: 'my-group/my-project' + ref: master + file: '/templates/.gitlab-ci-template.yml' + + - project: 'my-group/my-project' + ref: v1.0.0 + file: '/templates/.gitlab-ci-template.yml' + + - project: 'my-group/my-project' + ref: 787123b47f14b552955ca2786bc9542ae66fee5b # Git SHA + file: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array +### `include:template` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53445) in GitLab 11.7. + +`include:template` can be used to include `.gitlab-ci.yml` templates that are +[shipped with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates). +For example: + +```yaml +# File sourced from GitLab's template collection include: - - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - local: '/templates/.after-script-template.yml' - template: Auto-DevOps.gitlab-ci.yml ``` ---- +### `include:remote` -`include` supports four types of files: +`include:remote` can be used to include a file from a different location, +using HTTP/HTTPS, referenced by using the full URL. The remote file must be +publicly accessible through a simple GET request as authentication schemas +in the remote URL is not supported. For example: -- **local** to the same repository, referenced by using full paths in the same - repository, with `/` being the root directory. For example: +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' +``` - ```yaml - # Within the repository - include: '/templates/.gitlab-ci-template.yml' - ``` +NOTE: **Note for GitLab admins:** +In order to include files from another repository inside your local network, +you may need to enable the **Allow requests to the local network from hooks and services** checkbox +located in the **Admin area > Settings > Network > Outbound requests** section. - Or using: +### `include` examples - ```yaml - # Within the repository - include: - local: '/templates/.gitlab-ci-template.yml' - ``` +Here are a few more `include` examples. - NOTE: **Note:** - You can only use files that are currently tracked by Git on the same branch - your configuration file is. In other words, when using a **local file**, make - sure that both `.gitlab-ci.yml` and the local file are on the same branch. +#### Single string or array of multiple values - NOTE: **Note:** - We don't support the inclusion of local files through Git submodules paths. +You can include your extra YAML file(s) either as a single string or +an array of multiple values. The following examples are all valid. -- **file** from another repository, referenced by using full paths in the same - repository, with `/` being the root directory. For example: +Single string with the `include:local` method implied: - ```yaml - include: - project: 'my-group/my-project' - file: '/templates/.gitlab-ci-template.yml' - ``` +```yaml +include: '/templates/.after-script-template.yml' +``` - You can also specify `ref:`. The default `ref:` is the `HEAD` of the project: +Array with `include` method implied: - ```yaml - include: - - project: 'my-group/my-project' - ref: master - file: '/templates/.gitlab-ci-template.yml' +```yaml +include: + - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - '/templates/.after-script-template.yml' +``` - - project: 'my-group/my-project' - ref: v1.0.0 - file: '/templates/.gitlab-ci-template.yml' +Single string with `include` method specified explicitly: - - project: 'my-group/my-project' - ref: 787123b47f14b552955ca2786bc9542ae66fee5b # git sha - file: '/templates/.gitlab-ci-template.yml' - ``` +```yaml +include: + remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +``` -- **remote** in a different location, accessed using HTTP/HTTPS, referenced - using the full URL. For example: +Array with `include:remote` being the single item: - ```yaml - # File sourced from outside repository - include: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' - ``` +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +``` - Or using: +Array with multiple `include` methods specified explicitly: - ```yaml - # File sourced from outside repository - include: - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' - ``` +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - local: '/templates/.after-script-template.yml' + - template: Auto-DevOps.gitlab-ci.yml +``` - NOTE: **Note:** - The remote file must be publicly accessible through a simple GET request, as we don't support authentication schemas in the remote URL. +Array mixed syntax: + +```yaml +include: + - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - '/templates/.after-script-template.yml' + - template: Auto-DevOps.gitlab-ci.yml + - project: 'my-group/my-project' + ref: master + file: '/templates/.gitlab-ci-template.yml' +``` - NOTE: **Note:** - In order to include files from another repository inside your local network, - you may need to enable the **Allow requests to the local network from hooks and services** checkbox - located in the **Settings > Network > Outbound requests** section within the **Admin area**. +#### Re-using a `before_script` template -- **template** included with GitLab. For example: +In the following example, the content of `.before-script-template.yml` will be +automatically fetched and evaluated along with the content of `.gitlab-ci.yml`. - ```yaml - # File sourced from GitLab's template collection - include: - template: Auto-DevOps.gitlab-ci.yml - ``` +Content of `https://gitlab.com/awesome-project/raw/master/.before-script-template.yml`: - NOTE: **Note:** - Templates included this way are sourced from [lib/gitlab/ci/templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates). +```yaml +before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - gem install bundler --no-document + - bundle install --jobs $(nproc) "${FLAGS[@]}" +``` ---- +Content of `.gitlab-ci.yml`: +```yaml +include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' -Since GitLab 10.8 we are now deep merging the files defined in `include` -with those in `.gitlab-ci.yml`. Files defined by `include` are always -evaluated first and merged with the content of `.gitlab-ci.yml`, no -matter the position of the `include` keyword. You can take advantage of -merging to customize and override details in included CI -configurations with local definitions. +rspec: + script: + - bundle exec rspec +``` -NOTE: **Note:** -The recursive includes are not supported, meaning your external files -should not use the `include` keyword, as it will be ignored. +#### Overriding external template values The following example shows specific YAML-defined variables and details of the `production` job from an include file being customized in `.gitlab-ci.yml`. -```yaml -# Content of https://company.com/autodevops-template.yml +Content of `https://company.com/autodevops-template.yml`: +```yaml variables: POSTGRES_USER: user POSTGRES_PASSWORD: testing_password @@ -1849,9 +1756,9 @@ production: - master ``` -```yaml -# Content of .gitlab-ci.yml +Content of `.gitlab-ci.yml`: +```yaml include: 'https://company.com/autodevops-template.yml' image: alpine:latest @@ -1878,11 +1785,11 @@ with the environment url of the `production` job defined in The merging lets you extend and override dictionary mappings, but you cannot add or modify items to an included array. For example, to add an additional item to the production job script, you must repeat the -existing script items. +existing script items: -```yaml -# Content of https://company.com/autodevops-template.yml +Content of `https://company.com/autodevops-template.yml`: +```yaml production: stage: production script: @@ -1890,9 +1797,9 @@ production: - deploy ``` -```yaml -# Content of .gitlab-ci.yml +Content of `.gitlab-ci.yml`: +```yaml include: 'https://company.com/autodevops-template.yml' stages: @@ -1909,10 +1816,140 @@ In this case, if `install_dependencies` and `deploy` were not repeated in `.gitlab-ci.yml`, they would not be part of the script for the `production` job in the combined CI configuration. +## `extends` + +> Introduced in GitLab 11.3. + +`extends` defines an entry name that a job that uses `extends` is going to +inherit from. + +It is an alternative to using [YAML anchors](#anchors) and is a little +more flexible and readable: + +```yaml +.tests: + script: rake test + stage: test + only: + refs: + - branches + +rspec: + extends: .tests + script: rake rspec + only: + variables: + - $RSPEC +``` + +In the example above, the `rspec` job inherits from the `.tests` template job. +GitLab will perform a reverse deep merge based on the keys. GitLab will: + +- Merge the `rspec` contents into `.tests` recursively. +- Not merge the values of the keys. + +This results in the following `rspec` job: + +```yaml +rspec: + script: rake rspec + stage: test + only: + refs: + - branches + variables: + - $RSPEC +``` + NOTE: **Note:** -We currently do not support using YAML aliases across different YAML files -sourced by `include`. You must only refer to aliases in the same file. Instead -of using YAML anchors you can use [`extends` keyword](#extends). +Note that `script: rake test` has been overwritten by `script: rake rspec`. + +If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script). + +`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it's +possible to inherit from regular jobs as well. + +`extends` supports multi-level inheritance, however it is not recommended to +use more than three levels. The maximum nesting level that is supported is 10. +The following example has two levels of inheritance: + +```yaml +.tests: + only: + - pushes + +.rspec: + extends: .tests + script: rake rspec + +rspec 1: + variables: + RSPEC_SUITE: '1' + extends: .rspec + +rspec 2: + variables: + RSPEC_SUITE: '2' + extends: .rspec + +spinach: + extends: .tests + script: rake spinach +``` + +## Using `extends` and `include` together + +`extends` works across configuration files combined with `include`. + +For example, if you have a local `included.yml` file: + +```yaml +.template: + script: + - echo Hello! +``` + +Then, in `.gitlab-ci.yml` you can use it like this: + +```yaml +include: included.yml + +useTemplate: + image: alpine + extends: .template +``` + +This will run a job called `useTemplate` that runs `echo Hello!` as defined in +the `.template` job, and uses the `alpine` Docker image as defined in the local job. + +## `pages` + +`pages` is a special job that is used to upload static content to GitLab that +can be used to serve your website. It has a special syntax, so the two +requirements below must be met: + +- Any static content must be placed under a `public/` directory. +- `artifacts` with a path to the `public/` directory must be defined. + +The example below simply moves all files from the root of the project to the +`public/` directory. The `.public` workaround is so `cp` doesn't also copy +`public/` to itself in an infinite loop: + +```yaml +pages: + stage: deploy + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). ## `variables` @@ -1951,9 +1988,9 @@ which can be set in GitLab's UI. ### Git strategy -> Introduced in GitLab 8.9 as an experimental feature. May change or be removed - completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner - v1.7+. +> Introduced in GitLab 8.9 as an experimental feature. May change or be removed +> completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner +> v1.7+. You can set the `GIT_STRATEGY` used for getting recent application code, either globally or per-job in the [`variables`](#variables) section. If left @@ -1989,6 +2026,11 @@ variables: GIT_STRATEGY: none ``` +NOTE: **Note:** `GIT_STRATEGY` is not supported for +[Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html), +but may be in the future. See the [support Git strategy with Kubernetes executor feature proposal](https://gitlab.com/gitlab-org/gitlab-runner/issues/3847) +for updates. + ### Git submodule strategy > Requires GitLab Runner v1.10+. @@ -2283,8 +2325,9 @@ capitalization, the commit will be created but the pipeline will be skipped. Alternatively, one can pass the `ci.skip` [Git push option][push-option] if using Git 2.10 or newer: -``` -$ git push -o ci.skip + +```sh +git push -o ci.skip ``` ## Validate the .gitlab-ci.yml diff --git a/doc/development/README.md b/doc/development/README.md index f22dde32de9..05715274a81 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -47,6 +47,7 @@ description: 'Learn how to contribute to GitLab.' - [Avoid modules with instance variables](module_with_instance_variables.md) if possible - [How to dump production data to staging](db_dump.md) - [Working with the GitHub importer](github_importer.md) +- [Import/Export development documentation](import_export.md) - [Working with Merge Request diffs](diffs.md) - [Permissions](permissions.md) - [Prometheus metrics](prometheus_metrics.md) diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 7c7da50a149..24feb1378a1 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -6,7 +6,7 @@ scheduling into milestones. Labelling is a task for everyone. Most issues will have labels for at least one of the following: -- Type: ~"feature proposal", ~bug, ~customer, etc. +- Type: ~feature, ~bug, ~customer, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Team: ~Plan, ~Manage, ~Quality, etc. - Stage: ~"devops:plan", ~"devops:create", etc. @@ -27,8 +27,8 @@ labels, you can _always_ add the team and type, and often also the subject. Type labels are very important. They define what kind of issue this is. Every issue should have one or more. -Examples of type labels are ~"feature proposal", ~bug, ~customer, ~security, -and ~"direction". +Examples of type labels are ~feature, ~bug, ~customer, ~security, +and ~direction. A number of type labels have a priority assigned to them, which automatically makes them float to the top, depending on their importance. @@ -67,7 +67,7 @@ The current team labels are: - ~Geo - ~Gitaly - ~Manage -- ~Monitoring +- ~Monitor - ~Plan - ~Quality - ~Release @@ -200,7 +200,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" +- Small ~feature - Small ~"technical debt" issues After adding the ~"Accepting merge requests" label, we try to estimate the @@ -259,10 +259,10 @@ For feature proposals for EE, open an issue on the [issue tracker of EE][ee-tracker]. In order to help track the feature proposals, we have created a -[`feature proposal`][fpl] label. For the time being, users that are not members +[`feature`][fl] label. For the time being, users that are not members of the project cannot add labels. You can instead ask one of the [core team] -members to add the label ~"feature proposal" to the issue or add the following -code snippet right after your description in a new line: `~"feature proposal"`. +members to add the label ~feature to the issue or add the following +code snippet right after your description in a new line: `~feature`. Please keep feature proposals as small and simple as possible, complex ones might be edited to make them small and simple. @@ -276,7 +276,7 @@ need to ask one of the [core team] members to add the label, if you do not have If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. -[fpl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature+proposal +[fl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature ## Issue tracker guidelines diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 721e26f2de9..c188819560e 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -272,20 +272,26 @@ Inside the document: - For regular code blocks, always use a highlighting class corresponding to the language for better readability. Examples: - ```md - ```ruby - Ruby code - ``` - - ```js - JavaScript code - ``` - - ```md - Markdown code - ``` - ``` - + ````md + ```ruby + Ruby code + ``` + + ```js + JavaScript code + ``` + + ```md + Markdown code + ``` + + ```text + Code for which no specific highlighting class is available. + ``` + ```` + +- To display raw markdown instead of rendered markdown, use four backticks on their own lines around the + markdown to display. See [example](https://gitlab.com/gitlab-org/gitlab-ce/blob/8c1991b9bb7e3b8d606481fdea316d633cfa5eb7/doc/development/documentation/styleguide.md#L275-287). - For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks). ## Alert boxes @@ -383,7 +389,7 @@ Which renders to: > ### This is an `h3` >{:.no_toc} -## Specific sections and terms +## Terms To maintain consistency through GitLab documentation, the following guides documentation authors on agreed styles and usage of terms. @@ -412,7 +418,7 @@ The following are recommended verbs for specific uses. |:------------|:--------------------------------|:-------------------| | "go" | making a browser go to location | "navigate", "open" | -### GitLab versions and tiers +## GitLab versions and tiers - Every piece of documentation that comes with a new feature should declare the GitLab version that feature got introduced. Right below the heading add a @@ -437,7 +443,7 @@ The following are recommended verbs for specific uses. > [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. ``` -#### Early versions of EE +### Early versions of EE If the feature was created before GitLab 9.2 (before [different EE tiers were introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1851)): @@ -450,7 +456,7 @@ For example: > [Introduced](<link-to-issue>) in GitLab Enterprise Edition 9.0. Available in [GitLab Premium](https://about.gitlab.com/pricing/). ``` -### Product badges +## Product badges When a feature is available in EE-only tiers, add the corresponding tier according to the feature availability: @@ -471,12 +477,16 @@ keyword "only": The tier should be ideally added to headers, so that the full badge will be displayed. However, it can be also mentioned from paragraphs, list items, and table cells. For these cases, the tier mention will be represented by an orange question mark that will show the tiers on hover. -E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**. + +For example: + +- `**[STARTER]**` renders as **[STARTER]** +- `**[STARTER ONLY]**` renders as **[STARTER ONLY]** The absence of tiers' mentions mean that the feature is available in GitLab Core, GitLab.com Free, and all higher tiers. -#### How it works +### How it works Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244), the special markup `**[STARTER]**` will generate a `span` element to trigger the diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index ad87ecf1b87..3a3cb77f592 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -21,8 +21,8 @@ We also use [Axios][axios] to handle all of our network requests. We also utilize [webpack][webpack] to handle the bundling, minification, and compression of our assets. -Working with our frontend assets requires Node (v6.0 or greater) and Yarn -(v1.2 or greater). You can find information on how to install these on our +Working with our frontend assets requires Node (v8.10.0 or greater) and Yarn +(v1.10.0 or greater). You can find information on how to install these on our [installation guide][install]. ### Browser Support diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 65963b959f7..4b60ec80cb8 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -120,8 +120,8 @@ create: 1. An action `receiveSomethingError`, to handle the error callback 1. An action `fetchSomething` to make the request. 1. In case your application does more than a `GET` request you can use these as examples: - - `PUT`: `createSomething` - - `POST`: `updateSomething` + - `POST`: `createSomething` + - `PUT`: `updateSomething` - `DELETE`: `deleteSomething` The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component diff --git a/doc/development/import_export.md b/doc/development/import_export.md new file mode 100644 index 00000000000..71db1abb201 --- /dev/null +++ b/doc/development/import_export.md @@ -0,0 +1,352 @@ +# Import/Export development documentation + +Troubleshooing and general development guidelines and tips for the [Import/Export feature](../user/project/settings/import_export.md). + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> This document is originally based on the [Import/Export 201 presentation available on YouTube](https://www.youtube.com/watch?v=V3i1OfExotE). + +## Troubleshooting commands + +Finds information about the status of the import and further logs using the JID: + +```ruby +# Rails console +Project.find_by_full_path('group/project').import_state.slice(:jid, :status, :last_error) +> {"jid"=>"414dec93f941a593ea1a6894", "status"=>"finished", "last_error"=>nil} +``` + +```bash +# Logs +grep JID /var/log/gitlab/sidekiq/current +grep "Import/Export error" /var/log/gitlab/sidekiq/current +grep "Import/Export backtrace" /var/log/gitlab/sidekiq/current +``` + +## Troubleshooting performance issues + +Read through the current performance problems using the Import/Export below. + +### OOM errors + +Out of memory (OOM) errors are normally caused by the [Sidekiq Memory Killer](https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html): + +```bash +SIDEKIQ_MEMORY_KILLER_MAX_RSS = 2GB in GitLab.com +``` + +An import status `started`, and the following sidekiq logs will signal a memory issue: + +```bash +WARN: Work still in progress <struct with JID> +``` + +### Timeouts + +Timeout errors occur due to the `StuckImportJobsWorker` marking the process as failed: + +```ruby +class StuckImportJobsWorker + include ApplicationWorker + include CronjobQueue + + IMPORT_JOBS_EXPIRATION = 15.hours.to_i + + def perform + import_state_without_jid_count = mark_import_states_without_jid_as_failed! + import_state_with_jid_count = mark_import_states_with_jid_as_failed! + ... +``` + +```bash +Marked stuck import jobs as failed. JIDs: xyz +``` + +``` + +-----------+ +-----------------------------------+ + |Export Job |--->| Calls ActiveRecord `as_json` and | + +-----------+ | `to_json` on all project models | + +-----------------------------------+ + + +-----------+ +-----------------------------------+ + |Import Job |--->| Loads all JSON in memory, then | + +-----------+ | inserts into the DB in batches | + +-----------------------------------+ +``` + +### Problems and solutions + +| Problem | Possible solutions | +| -------- | -------- | +| [Slow JSON](https://gitlab.com/gitlab-org/gitlab-ce/issues/54084) loading/dumping models from the database | [split the worker](https://gitlab.com/gitlab-org/gitlab-ce/issues/54085) | +| | Batch export +| | Optimize SQL +| | Move away from `ActiveRecord` callbacks (difficult) +| High memory usage (see also some [analysis](https://gitlab.com/gitlab-org/gitlab-ce/issues/35389) | DB Commit sweet spot that uses less memory | +| | [Netflix Fast JSON API](https://github.com/Netflix/fast_jsonapi) may help | +| | Batch reading/writing to disk and any SQL + +### Temporary solutions + +While the performance problems are not tackled, there is a process to workaround +importing big projects, using a foreground import: + +[Foreground import](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5384) of big projects for customers. +(Using the import template in the [infrastructure tracker](https://gitlab.com/gitlab-com/gl-infra/infrastructure/)) + +## Security + +The Import/Export feature is constantly updated (adding new things to export), however +the code hasn't been refactored in a long time. We should perform a [code audit](https://gitlab.com/gitlab-org/gitlab-ce/issues/42135) +to make sure its dynamic nature does not increase the number of security concerns. + +### Security in the code + +Some of these classes provide a layer of security to the Import/Export. + +The `AttributeCleaner` removes any prohibited keys: + +```ruby +# AttributeCleaner +# Removes all `_ids` and other prohibited keys + class AttributeCleaner + ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] + + def clean + @relation_hash.reject do |key, _value| + prohibited_key?(key) || !@relation_class.attribute_method?(key) || excluded_key?(key) + end.except('id') + end + + ... + +``` + +The `AttributeConfigurationSpec` checks and confirms the addition of new columns: + +```ruby +# AttributeConfigurationSpec +<<-MSG + It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: + + Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported. + Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent + model in the +excluded_attributes+ section. + + SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)} + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} +MSG +``` + +The `ModelConfigurationSpec` checks and confirms the addition of new models: + +```ruby +# ModelConfigurationSpec +<<-MSG + New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by + the Import/Export feature. + + If you think this model should be included in the export, please add it to `#{Gitlab::ImportExport.config_file}`. + + Definitely add it to `#{File.expand_path(ce_models_yml)}` + #{"or `#{File.expand_path(ee_models_yml)}` if the model/associations are EE-specific\n" if ee_models_hash.any?} + to signal that you've handled this error and to prevent it from showing up in the future. +MSG +``` + +The `ExportFileSpec` detects encrypted or sensitive columns: + +```ruby +# ExportFileSpec +<<-MSG + Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect} + If you think this information shouldn't get exported, please exclude the model or attribute in + IMPORT_EXPORT_CONFIG. + + Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the + key and the correspondent hash or model as the value. + + Also, if the attribute is a generated unique token, please add it to RelationFactory::TOKEN_RESET_MODELS + if it needs to be reset (to prevent duplicate column problems while importing to the same instance). + + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} + CURRENT_SPEC: #{__FILE__} +MSG +``` + +## Versioning + +Import/Export does not use strict SemVer, since it has frequent constant changes +during a single GitLab release. It does require an update when there is a breaking change. + +```ruby +# ImportExport +module Gitlab + module ImportExport + extend self + + # For every version update, the version history in import_export.md has to be kept up to date. + VERSION = '0.2.4' +``` + +## Version history + +The [current version history](../user/project/settings/import_export.md) also displays the equivalent GitLab version +and it is useful for knowing which versions won't be compatible between them. + +| GitLab version | Import/Export version | +| ---------------- | --------------------- | +| 11.1 to current | 0.2.4 | +| 10.8 | 0.2.3 | +| 10.4 | 0.2.2 | +| ... | ... | +| 8.10.3 | 0.1.3 | +| 8.10.0 | 0.1.2 | +| 8.9.5 | 0.1.1 | +| 8.9.0 | 0.1.0 | + +### When to bump the version up + +We will have to bump the verision if we rename model/columns or perform any format +modifications in the JSON structure or the file structure of the archive file. + +We do not need to bump the version up in any of the following cases: + +- Add a new column or a model +- Remove a column or model (unless there is a DB constraint) +- Export new things (such as a new type of upload) + + +Every time we bump the version, the integration specs will fail and can be fixed with: + +```bash +bundle exec rake gitlab:import_export:bump_version +``` + +### Renaming columns or models + +This is a relatively common occurence that will require a version bump. + +There is also the _RC problem_ - GitLab.com runs an RC, prior to any customers, +meaning that we want to bump the version up in the next version (or patch release). + +For example: + +1. Add rename to `RelationRenameService` in X.Y +2. Remove it from `RelationRenameService` in X.Y + 1 +3. Bump Import/Export version in X.Y + 1 + +```ruby +module Gitlab + module ImportExport + class RelationRenameService + RENAMES = { + 'pipelines' => 'ci_pipelines' # Added in 11.6, remove in 11.7 + }.freeze +``` + +## A quick dive into the code + +### Import/Export configuration (`import_export.yml`) + +The main configuration `import_export.yml` defines what models can be exported/imported. + +Model relationships to be included in the project import/export: + +```yaml +project_tree: + - labels: + :priorities + - milestones: + - events: + - :push_event_payload + - issues: + - events: + - ... +``` + +Only include the following attributes for the models specified: + +```yaml +included_attributes: + user: + - :id + - :email + ... + +``` + +Do not include the following attributes for the models specified: + +```yaml +excluded_attributes: + project: + - :name + - :path + - ... +``` + +Extra methods to be called by the export: + +```yaml +# Methods +methods: + labels: + - :type + label: + - :type +``` + +### Import + +The import job status moves from `none` to `finished` or `failed` into different states: + +_import\_status_: none -> scheduled -> started -> finished/failed + +While the status is `started` the `Importer` code processes each step required for the import. + +```ruby +# ImportExport::Importer +module Gitlab + module ImportExport + class Importer + def execute + if import_file && check_version! && restorers.all?(&:restore) && overwrite_project + project_tree.restored_project + else + raise Projects::ImportService::Error.new(@shared.errors.join(', ')) + end + rescue => e + raise Projects::ImportService::Error.new(e.message) + ensure + remove_import_file + end + + def restorers + [repo_restorer, wiki_restorer, project_tree, avatar_restorer, + uploads_restorer, lfs_restorer, statistics_restorer] + end +``` + +The export service, is similar to the `Importer`, restoring data instead of saving it. + +### Export + +```ruby +# ImportExport::ExportService +module Projects + module ImportExport + class ExportService < BaseService + + def save_all! + if save_services + Gitlab::ImportExport::Saver.save(project: project, shared: @shared) + notify_success + else + cleanup_and_notify_error! + end + end + + def save_services + [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, + wiki_repo_saver, lfs_saver].all?(&:save) + end +``` diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index bbb2313ea7b..f6bc9fb0979 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -5,6 +5,18 @@ It's a test that sometimes fails, but if you retry it enough times, it passes, eventually. +## Quarantined tests + +Tests can be put in quarantine by assigning `:quarantine` metadata. This means +they will be skipped unless run with `--tag quarantine`. This can be used for +tests that are expected to fail while a fix is in progress (similar to how +[`skip` or `pending`](https://relishapp.com/rspec/rspec-core/v/3-8/docs/pending-and-skipped-examples) + can be used). + +``` +bin/rspec --tag quarantine +``` + ## Automatic retries and flaky tests detection On our CI, we use [rspec-retry] to automatically retry a failing example a few diff --git a/doc/install/installation.md b/doc/install/installation.md index 5653c59f576..2eba2cc4847 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -50,62 +50,85 @@ The GitLab installation consists of setting up the following components: `sudo` is not installed on Debian by default. Make sure your system is up-to-date and install it. - # run as root! - apt-get update -y - apt-get upgrade -y - apt-get install sudo -y +```sh +# run as root! +apt-get update -y +apt-get upgrade -y +apt-get install sudo -y +``` **Note:** During this installation some files will need to be edited manually. If you are familiar with vim set it as default editor with the commands below. If you are not familiar with vim please skip this and keep using the default editor. - # Install vim and set as default editor - sudo apt-get install -y vim - sudo update-alternatives --set editor /usr/bin/vim.basic +```sh +# Install vim and set as default editor +sudo apt-get install -y vim +sudo update-alternatives --set editor /usr/bin/vim.basic +``` Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate rsync python-docutils pkg-config cmake +```sh +sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev \ + libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev \ + libxslt-dev libcurl4-openssl-dev libicu-dev logrotate rsync python-docutils pkg-config cmake +``` Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but you can [install re2 manually](https://github.com/google/re2/wiki/Install). If you want to use Kerberos for user authentication, then install libkrb5-dev: - sudo apt-get install libkrb5-dev +```sh +sudo apt-get install libkrb5-dev +``` **Note:** If you don't know what Kerberos is, you can assume you don't need it. Make sure you have the right version of Git installed - # Install Git - sudo apt-get install -y git-core +```sh +# Install Git +sudo apt-get install -y git-core - # Make sure Git is version 2.18.0 or higher - git --version +# Make sure Git is version 2.18.0 or higher +git --version +``` Is the system packaged Git too old? Remove it and compile from source. - # Remove packaged Git - sudo apt-get remove git-core +```sh +# Remove packaged Git +sudo apt-get remove git-core + +# Install dependencies +sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential + +# Download and compile from source +cd /tmp +curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz +echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz +cd git-2.18.0/ +./configure +make prefix=/usr/local all - # Install dependencies - sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential +# Install into /usr/local/bin +sudo make prefix=/usr/local install - # Download and compile from source - cd /tmp - curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz - echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz - cd git-2.18.0/ - ./configure - make prefix=/usr/local all +# When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git +``` - # Install into /usr/local/bin - sudo make prefix=/usr/local install +For the [Custom Favicon](../customization/favicon.md) to work, graphicsmagick +needs to be installed. - # When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git +```sh +sudo apt-get install -y graphicsmagick +``` **Note:** In order to receive mail notifications, make sure to install a mail server. By default, Debian is shipped with exim4 but this [has problems](https://gitlab.com/gitlab-org/gitlab-ce/issues/12754) while Ubuntu does not ship with one. The recommended mail server is postfix and you can install it with: - sudo apt-get install -y postfix +```sh +sudo apt-get install -y postfix +``` Then select 'Internet Site' and press enter to confirm the hostname. @@ -127,22 +150,28 @@ instructions are designed to install Ruby from the official source code. Remove the old Ruby 1.8 if present: - sudo apt-get remove ruby1.8 +```sh +sudo apt-get remove ruby1.8 +``` Download Ruby and compile it: - mkdir /tmp/ruby && cd /tmp/ruby - curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz - echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz - cd ruby-2.5.3 - - ./configure --disable-install-rdoc - make - sudo make install +```sh +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz +echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz +cd ruby-2.5.3 + +./configure --disable-install-rdoc +make +sudo make install +``` Then install the Bundler Gem: - sudo gem install bundler --no-document +```sh +sudo gem install bundler --no-document +``` ## 3. Go @@ -151,31 +180,35 @@ GitLab we need a Go compiler. The instructions below assume you use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). - # Remove former Go installation folder - sudo rm -rf /usr/local/go - - curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz - echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ - sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz - sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ - rm go1.10.3.linux-amd64.tar.gz +```sh +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz +echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.3.linux-amd64.tar.gz +``` ## 4. Node Since GitLab 8.17, GitLab requires the use of Node to compile javascript assets, and Yarn to manage javascript dependencies. The current minimum -requirements for these are node >= v6.0.0 and yarn >= v1.2.0. In many distros +requirements for these are node >= v8.10.0 and yarn >= v1.10.0. In many distros the versions provided by the official package repositories are out of date, so we'll need to install through the following commands: - # install node v8.x - curl --location https://deb.nodesource.com/setup_8.x | sudo bash - - sudo apt-get install -y nodejs - - curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - sudo apt-get update - sudo apt-get install yarn +```sh +# install node v8.x +curl --location https://deb.nodesource.com/setup_8.x | sudo bash - +sudo apt-get install -y nodejs + +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps. @@ -183,7 +216,9 @@ Visit the official websites for [node](https://nodejs.org/en/download/package-ma Create a `git` user for GitLab: - sudo adduser --disabled-login --gecos 'GitLab' git +```sh +sudo adduser --disabled-login --gecos 'GitLab' git +``` ## 6. Database @@ -195,37 +230,37 @@ you need at least PostgreSQL 9.2. 1. Install the database packages: - ```bash + ```sh sudo apt-get install -y postgresql postgresql-client libpq-dev postgresql-contrib ``` 1. Create a database user for GitLab: - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;" ``` 1. Create the `pg_trgm` extension (required for GitLab 8.6+): - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" ``` 1. Create the GitLab production database and grant all privileges on database: - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;" ``` 1. Try connecting to the new database with the new user: - ```bash + ```sh sudo -u git -H psql -d gitlabhq_production ``` 1. Check if the `pg_trgm` extension is enabled: - ```bash + ```sh SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' @@ -243,7 +278,7 @@ you need at least PostgreSQL 9.2. 1. Quit the database session: - ```bash + ```sh gitlabhq_production> \q ``` @@ -262,7 +297,7 @@ If you are using Debian 7 or Ubuntu 12.04, follow the special documentation on [an alternate Redis installation](redis.md). Once done, follow the rest of the guide here. -``` +```sh # Configure redis to use sockets sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig @@ -294,89 +329,95 @@ sudo usermod -aG redis git ## 8. GitLab - # We'll install GitLab into home directory of the user "git" - cd /home/git +```sh +# We'll install GitLab into home directory of the user "git" +cd /home/git +``` ### Clone the Source - # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab +```sh +# Clone GitLab repository +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab +``` **Note:** You can change `11-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It - # Go to GitLab installation folder - cd /home/git/gitlab +```sh +# Go to GitLab installation folder +cd /home/git/gitlab - # Copy the example GitLab config - sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml +# Copy the example GitLab config +sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml - # Update GitLab config file, follow the directions at top of file - sudo -u git -H editor config/gitlab.yml +# Update GitLab config file, follow the directions at top of file +sudo -u git -H editor config/gitlab.yml - # Copy the example secrets file - sudo -u git -H cp config/secrets.yml.example config/secrets.yml - sudo -u git -H chmod 0600 config/secrets.yml +# Copy the example secrets file +sudo -u git -H cp config/secrets.yml.example config/secrets.yml +sudo -u git -H chmod 0600 config/secrets.yml - # Make sure GitLab can write to the log/ and tmp/ directories - sudo chown -R git log/ - sudo chown -R git tmp/ - sudo chmod -R u+rwX,go-w log/ - sudo chmod -R u+rwX tmp/ +# Make sure GitLab can write to the log/ and tmp/ directories +sudo chown -R git log/ +sudo chown -R git tmp/ +sudo chmod -R u+rwX,go-w log/ +sudo chmod -R u+rwX tmp/ - # Make sure GitLab can write to the tmp/pids/ and tmp/sockets/ directories - sudo chmod -R u+rwX tmp/pids/ - sudo chmod -R u+rwX tmp/sockets/ +# Make sure GitLab can write to the tmp/pids/ and tmp/sockets/ directories +sudo chmod -R u+rwX tmp/pids/ +sudo chmod -R u+rwX tmp/sockets/ - # Create the public/uploads/ directory - sudo -u git -H mkdir public/uploads/ +# Create the public/uploads/ directory +sudo -u git -H mkdir public/uploads/ - # Make sure only the GitLab user has access to the public/uploads/ directory - # now that files in public/uploads are served by gitlab-workhorse - sudo chmod 0700 public/uploads +# Make sure only the GitLab user has access to the public/uploads/ directory +# now that files in public/uploads are served by gitlab-workhorse +sudo chmod 0700 public/uploads - # Change the permissions of the directory where CI job traces are stored - sudo chmod -R u+rwX builds/ +# Change the permissions of the directory where CI job traces are stored +sudo chmod -R u+rwX builds/ - # Change the permissions of the directory where CI artifacts are stored - sudo chmod -R u+rwX shared/artifacts/ +# Change the permissions of the directory where CI artifacts are stored +sudo chmod -R u+rwX shared/artifacts/ - # Change the permissions of the directory where GitLab Pages are stored - sudo chmod -R ug+rwX shared/pages/ +# Change the permissions of the directory where GitLab Pages are stored +sudo chmod -R ug+rwX shared/pages/ - # Copy the example Unicorn config - sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb +# Copy the example Unicorn config +sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb - # Find number of cores - nproc +# Find number of cores +nproc - # Enable cluster mode if you expect to have a high load instance - # Set the number of workers to at least the number of cores - # Ex. change amount of workers to 3 for 2GB RAM server - sudo -u git -H editor config/unicorn.rb +# Enable cluster mode if you expect to have a high load instance +# Set the number of workers to at least the number of cores +# Ex. change amount of workers to 3 for 2GB RAM server +sudo -u git -H editor config/unicorn.rb - # Copy the example Rack attack config - sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb +# Copy the example Rack attack config +sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user - # 'autocrlf' is needed for the web editor - sudo -u git -H git config --global core.autocrlf input +# Configure Git global settings for git user +# 'autocrlf' is needed for the web editor +sudo -u git -H git config --global core.autocrlf input - # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed - sudo -u git -H git config --global gc.auto 0 +# Disable 'git gc --auto' because GitLab already runs 'git gc' when needed +sudo -u git -H git config --global gc.auto 0 - # Enable packfile bitmaps - sudo -u git -H git config --global repack.writeBitmaps true +# Enable packfile bitmaps +sudo -u git -H git config --global repack.writeBitmaps true - # Enable push options - sudo -u git -H git config --global receive.advertisePushOptions true +# Enable push options +sudo -u git -H git config --global receive.advertisePushOptions true - # Configure Redis connection settings - sudo -u git -H cp config/resque.yml.example config/resque.yml +# Configure Redis connection settings +sudo -u git -H cp config/resque.yml.example config/resque.yml - # Change the Redis socket path if you are not using the default Debian / Ubuntu configuration - sudo -u git -H editor config/resque.yml +# Change the Redis socket path if you are not using the default Debian / Ubuntu configuration +sudo -u git -H editor config/resque.yml +``` **Important Note:** Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. @@ -384,33 +425,37 @@ sudo usermod -aG redis git ### Configure GitLab DB Settings - # PostgreSQL only: - sudo -u git cp config/database.yml.postgresql config/database.yml - - # MySQL only: - sudo -u git cp config/database.yml.mysql config/database.yml - - # MySQL and remote PostgreSQL only: - # Update username/password in config/database.yml. - # You only need to adapt the production settings (first part). - # If you followed the database guide then please do as follows: - # Change 'secure password' with the value you have given to $password - # You can keep the double quotes around the password - sudo -u git -H editor config/database.yml - - # PostgreSQL and MySQL: - # Make config/database.yml readable to git only - sudo -u git -H chmod o-rwx config/database.yml +```sh +# PostgreSQL only: +sudo -u git cp config/database.yml.postgresql config/database.yml + +# MySQL only: +sudo -u git cp config/database.yml.mysql config/database.yml + +# MySQL and remote PostgreSQL only: +# Update username/password in config/database.yml. +# You only need to adapt the production settings (first part). +# If you followed the database guide then please do as follows: +# Change 'secure password' with the value you have given to $password +# You can keep the double quotes around the password +sudo -u git -H editor config/database.yml + +# PostgreSQL and MySQL: +# Make config/database.yml readable to git only +sudo -u git -H chmod o-rwx config/database.yml +``` ### Install Gems **Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. - # For PostgreSQL (note, the option says "without ... mysql") - sudo -u git -H bundle install --deployment --without development test mysql aws kerberos +```sh +# For PostgreSQL (note, the option says "without ... mysql") +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos - # Or if you use MySQL (note, the option says "without ... postgres") - sudo -u git -H bundle install --deployment --without development test postgres aws kerberos +# Or if you use MySQL (note, the option says "without ... postgres") +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos +``` **Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above. @@ -418,12 +463,14 @@ sudo usermod -aG redis git GitLab Shell is an SSH access and repository management software developed specially for GitLab. - # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true +```sh +# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): +sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true - # By default, the gitlab-shell config is generated from your main GitLab config. - # You can review (and modify) the gitlab-shell config as follows: - sudo -u git -H editor /home/git/gitlab-shell/config.yml +# By default, the gitlab-shell config is generated from your main GitLab config. +# You can review (and modify) the gitlab-shell config as follows: +sudo -u git -H editor /home/git/gitlab-shell/config.yml +``` **Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. @@ -441,57 +488,73 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse` which is the recommended location. - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` You can specify a different Git repository by providing it as an extra parameter: - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production +``` ### Install gitlab-pages GitLab-Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab-Pages in `/home/git/gitlab-pages`. For additional setup steps, please consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be ran several different ways. - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) - sudo -u git -H make +```sh +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git +cd gitlab-pages +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` ### Install Gitaly - # Fetch Gitaly source with Git and compile with Go - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production +```sh +# Fetch Gitaly source with Git and compile with Go +sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production +``` You can specify a different Git repository by providing it as an extra parameter: - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production +``` Next, make sure gitaly configured: - # Restrict Gitaly socket access - sudo chmod 0700 /home/git/gitlab/tmp/sockets/private - sudo chown git /home/git/gitlab/tmp/sockets/private +```sh +# Restrict Gitaly socket access +sudo chmod 0700 /home/git/gitlab/tmp/sockets/private +sudo chown git /home/git/gitlab/tmp/sockets/private - # If you are using non-default settings you need to update config.toml - cd /home/git/gitaly - sudo -u git -H editor config.toml +# If you are using non-default settings you need to update config.toml +cd /home/git/gitaly +sudo -u git -H editor config.toml +``` For more information about configuring Gitaly see [doc/administration/gitaly](../administration/gitaly). ### Initialize Database and Activate Advanced Features - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production - # Type 'yes' to create the database tables. +```sh +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production +# Type 'yes' to create the database tables. - # or you can skip the question by adding force=yes - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes +# or you can skip the question by adding force=yes +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes - # When done you see 'Administrator account created:' +# When done you see 'Administrator account created:' +``` **Note:** You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password. - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail +```sh +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail +``` ### Secure secrets.yml @@ -503,43 +566,58 @@ Otherwise your secrets are exposed if one of your backups is compromised. Download the init script (will be `/etc/init.d/gitlab`): - sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +```sh +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` And if you are installing with a non-default folder or user copy and edit the defaults file: - sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab +```sh +sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab +``` If you installed GitLab in another directory or as a user other than the default you should change these settings in `/etc/default/gitlab`. Do not edit `/etc/init.d/gitlab` as it will be changed on upgrade. Make GitLab start on boot: - sudo update-rc.d gitlab defaults 21 +```sh +sudo update-rc.d gitlab defaults 21 +``` ### Set up Logrotate - sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab +```sh +sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab +``` ### Check Application Status Check if GitLab and its environment are configured correctly: - sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production - +```sh +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` ### Compile GetText PO files - sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production +``` ### Compile Assets - sudo -u git -H yarn install --production --pure-lockfile - sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production +```sh +sudo -u git -H yarn install --production --pure-lockfile +sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production +``` ### Start Your GitLab Instance - sudo service gitlab start - # or - sudo /etc/init.d/gitlab restart +```sh +sudo service gitlab start +# or +sudo /etc/init.d/gitlab restart +``` ## 9. Nginx @@ -547,27 +625,33 @@ Check if GitLab and its environment are configured correctly: ### Installation - sudo apt-get install -y nginx +```sh +sudo apt-get install -y nginx +``` ### Site Configuration Copy the example site config: - sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab - sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab +```sh +sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab +sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab +``` Make sure to edit the config file to match your setup. Also, ensure that you match your paths to GitLab, especially if installing for a user other than the 'git' user: - # Change YOUR_SERVER_FQDN to the fully-qualified - # domain name of your host serving GitLab. - # - # Remember to match your paths to GitLab, especially - # if installing for a user other than 'git'. - # - # If using Ubuntu default nginx install: - # either remove the default_server from the listen line - # or else sudo rm -f /etc/nginx/sites-enabled/default - sudo editor /etc/nginx/sites-available/gitlab +```sh +# Change YOUR_SERVER_FQDN to the fully-qualified +# domain name of your host serving GitLab. +# +# Remember to match your paths to GitLab, especially +# if installing for a user other than 'git'. +# +# If using Ubuntu default nginx install: +# either remove the default_server from the listen line +# or else sudo rm -f /etc/nginx/sites-enabled/default +sudo editor /etc/nginx/sites-available/gitlab +``` If you intend to enable GitLab pages, there is a separate Nginx config you need to use. Read all about the needed configuration at the @@ -579,13 +663,17 @@ to use. Read all about the needed configuration at the Validate your `gitlab` or `gitlab-ssl` Nginx config file with the following command: - sudo nginx -t +```sh +sudo nginx -t +``` You should receive `syntax is okay` and `test is successful` messages. If you receive errors check your `gitlab` or `gitlab-ssl` Nginx config file for typos, etc. as indicated in the error message given. ### Restart - sudo service nginx restart +```sh +sudo service nginx restart +``` ## Done! @@ -593,7 +681,9 @@ You should receive `syntax is okay` and `test is successful` messages. If you re To make sure you didn't miss anything run a more thorough check with: - sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` If all items are green, then congratulations on successfully installing GitLab! @@ -680,31 +770,39 @@ for the changes to take effect. If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. - # example - production: - url: redis://redis.example.tld:6379 +``` +# example +production: + url: redis://redis.example.tld:6379 +``` If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file. - # example - production: - url: unix:/path/to/redis/socket +``` +# example +production: + url: unix:/path/to/redis/socket +``` Also you can use environment variables in the `config/resque.yml` file: - # example - production: - url: <%= ENV.fetch('GITLAB_REDIS_URL') %> +``` +# example +production: + url: <%= ENV.fetch('GITLAB_REDIS_URL') %> +``` ### Custom SSH Connection If you are running SSH on a non-standard port, you must change the GitLab user's SSH config. - # Add to /home/git/.ssh/config - host localhost # Give your setup a name (here: override localhost) - user git # Your remote git user - port 2222 # Your port number - hostname 127.0.0.1; # Your server name or IP +``` +# Add to /home/git/.ssh/config +host localhost # Give your setup a name (here: override localhost) + user git # Your remote git user + port 2222 # Your port number + hostname 127.0.0.1; # Your server name or IP +``` You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`, `admin_uri`) in the `config\gitlab.yml` file. diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index b17b0a4bc4a..fb2b6768f0a 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -12,7 +12,7 @@ If a web service does not require authentication, Webhooks can be used to trigge To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed. -This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`): +This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings/network`): ![Outbound requests admin settings](img/outbound_requests_section.png) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 780e9b8783e..68e50a61151 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -683,6 +683,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. | | `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); 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. This takes precedence over `CANARY_REPLICAS`; defaults to 1 | +| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. | +| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. | | `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. | diff --git a/doc/update/11.5-to-11.6.md b/doc/update/11.5-to-11.6.md index 2e9ec5d71de..33e09e1915b 100644 --- a/doc/update/11.5-to-11.6.md +++ b/doc/update/11.5-to-11.6.md @@ -317,11 +317,11 @@ sudo systemctl daemon-reload ```bash cd /home/git/gitlab -# MySQL installations (note: the line below states '--without postgres') -sudo -u git -H bundle install --without postgres development test --deployment - # PostgreSQL installations (note: the line below states '--without mysql') -sudo -u git -H bundle install --without mysql development test --deployment +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/doc/update/11.6-to-11.7.md b/doc/update/11.6-to-11.7.md index f5f671c1946..9e61d978631 100644 --- a/doc/update/11.6-to-11.7.md +++ b/doc/update/11.6-to-11.7.md @@ -66,7 +66,7 @@ from source at the nodejs.org website. <https://nodejs.org/en/download/> -GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript +GitLab also requires the use of yarn `>= v1.10.0` to manage JavaScript dependencies. ```bash @@ -317,11 +317,11 @@ sudo systemctl daemon-reload ```bash cd /home/git/gitlab -# MySQL installations (note: the line below states '--without postgres') -sudo -u git -H bundle install --without postgres development test --deployment - # PostgreSQL installations (note: the line below states '--without mysql') -sudo -u git -H bundle install --without mysql development test --deployment +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/doc/update/11.7-to-11.8.md b/doc/update/11.7-to-11.8.md new file mode 100644 index 00000000000..7ab98d80eb9 --- /dev/null +++ b/doc/update/11.7-to-11.8.md @@ -0,0 +1,394 @@ +--- +comments: false +--- + +# From 11.7 to 11.8 + +Make sure you view this update guide from the branch (version) of GitLab you would +like to install (e.g., `11-8-stable`. You can select the branch in the version +dropdown at the top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: Beginning in GitLab 11.0, we only support Ruby 2.4 or higher, and dropped +support for Ruby 2.3. Be sure to upgrade if necessary. + +You can check which version you are running with `ruby -v`. + +Download Ruby and compile it: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz +echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz +cd ruby-2.5.3 + +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-document +``` + +### 4. Update Node + +NOTE: Beginning in GitLab 11.8, we only support node 8 or higher, and dropped +support for node 6. Be sure to upgrade if necessary. + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v8.10.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v8.10.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +GitLab also requires the use of yarn `>= v1.10.0` to manage JavaScript +dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go +1.9.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://dl.google.com/go/go1.10.5.linux-amd64.tar.gz +echo 'a035d9beda8341b645d3f45a1b620cf2d8fb0c5eb409be36b389c0fd384ecc3a go1.10.5.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.5.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.5.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all --prune +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-8-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update gitlab-pages + +#### Only needed if you use GitLab Pages + +Install and compile gitlab-pages. GitLab-Pages uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-pages + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` + +### 11. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 12. Update configuration files + +#### New `unicorn.rb` configuration + +We have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future. + +Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/config/unicorn.rb.example but with your settings. +In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below: + +```ruby +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + +before_exec do |server| + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do |server, worker| + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +after_fork do |server, worker| + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end +``` + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/11-7-stable:config/gitlab.yml.example origin/11-8-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/11-7-stable:lib/support/nginx/gitlab-ssl origin/11-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/11-7-stable:lib/support/nginx/gitlab origin/11-8-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/11-7-stable:lib/support/init.d/gitlab.default.example origin/11-8-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 13. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos + + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 14. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 15. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (11.7) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 11.6 to 11.7](11.6-to-11.7.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md index 93be3da44d4..e4fd08a582c 100644 --- a/doc/user/award_emojis.md +++ b/doc/user/award_emojis.md @@ -1,24 +1,24 @@ # Award emoji -> **Notes:** -> - First [introduced][1825] in GitLab 8.2. -> - GitLab 9.0 [introduced][ce-9570] the usage of native emojis if the platform +> - First [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825) in GitLab 8.2. +> - GitLab 9.0 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570) the usage of native emoji if the platform > supports them and falls back to images or CSS sprites. This change greatly -> improved the award emoji performance overall. +> improved award emoji performance overall. When you're collaborating online, you get fewer opportunities for high-fives -and thumbs-ups. Emoji can be awarded to issues, merge requests, snippets, and -virtually everywhere where you can have a discussion. +and thumbs-ups. Emoji can be awarded to [issues](project/issues/index.md), [merge requests](project/merge_requests/index.md), +[snippets](snippets.md), and anywhere you can have a discussion. ![Award emoji](img/award_emoji_select.png) Award emoji make it much easier to give and receive feedback without a long -comment thread. Comments that are only emoji will automatically become -award emoji. +comment thread. + +For information on the relevant API, see [Award Emoji API](../api/award_emoji.md). ## Sort issues and merge requests on vote count -> [Introduced][2871] in GitLab 8.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781) in GitLab 8.5. You can quickly sort issues and merge requests by the number of votes they have received. The sort options can be found in the dropdown menu as "Most @@ -32,20 +32,16 @@ downvotes. ## Award emoji for comments -> [Introduced][4291] in GitLab 8.9. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291) in GitLab 8.9. Award emoji can also be applied to individual comments when you want to celebrate an accomplishment or agree with an opinion. -To add an award emoji, click the smile in the top right of the comment and pick -an emoji from the dropdown. If you want to remove an award emoji, just click -the emoji again and the vote will be removed. +To: + +- Add an award emoji, click the smile in the top right of the comment and pick an emoji from the dropdown. +- Remove an award emoji, click the emoji again and the vote will be removed. ![Picking an emoji for a comment](img/award_emoji_comment_picker.png) ![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png) - -[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781 -[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825 -[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291 -[ce-9570]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570 diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 9379d047fca..84f4b0b3922 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -280,7 +280,7 @@ Additionally locked issues can not be reopened. For issues with many comments like activity notes and user comments, sometimes finding useful information can be hard. There is a way to filter comments from single notes and discussions for merge requests and issues. -From a merge request's **Discussion** tab, or from an issue overview, find the filter's dropdown menu on the right side of the page, from which you can choose one of the following options: +From a merge request's **Discussion** tab, or from an epic/issue overview, find the filter's dropdown menu on the right side of the page, from which you can choose one of the following options: - **Show all activity**: displays all user comments and system notes (issue updates, mentions from other issues, changes to the description, etc). diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 4d56b33f684..b6f8f55978b 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -164,9 +164,11 @@ and you can choose the group of people to be notified. Here's a list of what you can't do with subgroups: -- [GitLab Pages](../../project/pages/index.md) are not currently working for - projects hosted under a subgroup. That means that only projects hosted under - the first parent group will work. +- [GitLab Pages](../../project/pages/index.md) supports projects hosted under + a subgroup, but not subgroup websites. + That means that only the highest-level group supports + [group websites](../../project/pages/introduction.html#user-or-group-pages), + although you can have project websites under a subgroup. - It is not possible to share a project with a group that's an ancestor of the group the project is in. That means you can only share as you walk down the hierarchy. For example, `group/subgroup01/project` **cannot** be shared diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 893658290e5..f2448f240ca 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -300,7 +300,7 @@ You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/ If you are new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/fearful.png" width="20px" height="20px">. You can easily join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/family.png" width="20px" height="20px">. All you need to do is to look up one of the supported codes. -Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px"> +Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px"> Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 2a1c8cc5bc0..0c358390046 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -178,9 +178,7 @@ group. | Remove group | | | | | ✓ | | Manage group labels | | ✓ | ✓ | ✓ | ✓ | | Create/edit/delete group milestones | | | ✓ | ✓ | ✓ | -| View private group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ | -| View internal group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ | -| View public group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ | +| View group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ | | Create/edit group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ | | Delete group epic **[ULTIMATE]** | | | | | ✓ | | View group Audit Events | | | | | ✓ | diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 6f334af4fb7..bb815695cb1 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -178,8 +178,11 @@ When creating a cluster in GitLab, you will be asked if you would like to create [Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/) cluster, or a [Role-based access control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) one. -Whether ABAC or RBAC is enabled, GitLab will create the necessary -service accounts and privileges in order to install and run +NOTE: **Note:** +[RBAC](#role-based-access-control-rbac) is recommended and the GitLab default. + +Whether [ABAC](#attribute-based-access-control-abac) or [RBAC](#role-based-access-control-rbac) is enabled, +GitLab will create the necessary service accounts and privileges in order to install and run [GitLab managed applications](#installing-applications): - If GitLab is creating the cluster, a `gitlab` service account with diff --git a/doc/user/project/index.md b/doc/user/project/index.md index d46ae31580a..b8989f5ae14 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -65,8 +65,8 @@ common actions on issues or merge requests browse, and download job artifacts - [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job), timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more - - [GKE cluster integration](clusters/index.md): Connecting your GitLab project - with Google Kubernetes Engine + - [Kubernetes cluster integration](clusters/index.md): Connecting your GitLab project + with a Kubernetes cluster - [GitLab Pages](pages/index.md): Build, test, and deploy your static website with GitLab Pages @@ -149,3 +149,17 @@ When [renaming a user](../profile/index.md#changing-your-username), work after a rename, making any transition a lot smoother. - The redirects will be available as long as the original path is not claimed by another group, user or project. + +## Use your project as a Go package + +Any project can be used as a Go package including private projects in subgroups. To use packages +hosted in private projects with the `go get` command, use a [`.netrc` file](https://ec.haxx.se/usingcurl-netrc.html) +and a personal access token in the password field. + +For example: + +```text +machine example.gitlab.com +login <gitlab_user_name> +password <personal_access_token> +``` diff --git a/doc/user/project/integrations/prometheus_library/index.md b/doc/user/project/integrations/prometheus_library/index.md index 9b9b4f6c8ca..a79bc2bce06 100644 --- a/doc/user/project/integrations/prometheus_library/index.md +++ b/doc/user/project/integrations/prometheus_library/index.md @@ -10,7 +10,8 @@ Currently supported exporters are: - [Kubernetes](kubernetes.md) - [NGINX](nginx.md) -- [NGINX Ingress Controller](nginx_ingress.md) +- [NGINX Ingress Controller 0.9.0-0.15.x](nginx_ingress_vts.md) +- [NGINX Ingress Controller 0.16.0+](nginx_ingress.md) - [HAProxy](haproxy.md) - [Amazon Cloud Watch](cloudwatch.md) diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index d5f77d622be..b7601f26802 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -1,8 +1,10 @@ # Monitoring NGINX Ingress Controller -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22133) in GitLab 11.7. -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-nginx/blob/master/Changelog.md#09-beta1) and above of the ingress. +NOTE: **Note:** NGINX Ingress versions prior to 0.16.0 offer an included [VTS Prometheus metrics exporter](nginx_ingress_vts.md), which exports metrics different than the built-in metrics. + +GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built-in Prometheus metrics included starting with [version 0.16.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#0160). ## Requirements @@ -12,9 +14,9 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI | Name | Query | | ---- | ----- | -| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) | -| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) | -| HTTP Error Rate (%) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100 | +| Throughput (req/sec) | sum(label_replace(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code) | +| Latency (ms) | sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 1000 | +| HTTP Error Rate (%) | sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 100 | ## Configuring NGINX ingress monitoring @@ -22,9 +24,9 @@ If you have deployed NGINX Ingress using GitLab's [Kubernetes cluster integratio For other deployments, there is [some configuration](#manually-setting-up-nginx-ingress-for-prometheus-monitoring) required depending on your installation: -- NGINX Ingress should be version 0.9.0 or above, with metrics enabled -- NGINX Ingress should be annotated for Prometheus monitoring -- Prometheus should be configured to monitor annotated pods +- NGINX Ingress should be version 0.16.0 or above, with metrics enabled. +- NGINX Ingress should be annotated for Prometheus monitoring. +- Prometheus should be configured to monitor annotated pods. ### About managed NGINX Ingress deployments @@ -32,9 +34,9 @@ NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [o NGINX is configured for Prometheus monitoring, by setting: -- `enable-vts-status: "true"`, to export Prometheus metrics -- `prometheus.io/scrape: "true"`, to enable automatic discovery -- `prometheus.io/port: "10254"`, to specify the metrics port +- `enable-vts-status: "true"`, to export Prometheus metrics. +- `prometheus.io/scrape: "true"`, to enable automatic discovery. +- `prometheus.io/port: "10254"`, to specify the metrics port. When used in conjunction with the GitLab deployed Prometheus service, response metrics will be automatically collected. @@ -51,6 +53,6 @@ Managing these settings depends on how NGINX ingress has been deployed. If you h ## Specifying the Environment label -In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`. +In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `ingress` label must `<CI_ENVIRONMENT_SLUG>`. If you have used [Auto Deploy](../../../../topics/autodevops/index.md#auto-deploy) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part. diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md new file mode 100644 index 00000000000..081eb8732ad --- /dev/null +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md @@ -0,0 +1,58 @@ +# Monitoring NGINX Ingress Controller with VTS metrics + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5. + +NOTE: **Note:** [NGINX Ingress version 0.16](nginx_ingress.md) and above have built-in Prometheus metrics, which are different than the VTS based metrics. + +GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the included VTS Prometheus metrics exporter in [version 0.9.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#09-beta1) through [0.15.x](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#0150). + +## Requirements + +[Prometheus integration](../prometheus.md) must be active. + +## Metrics supported + +| Name | Query | +| ---- | ----- | +| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) | +| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) | +| HTTP Error Rate (%) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100 | + +## Configuring NGINX ingress monitoring + +If you have deployed NGINX Ingress using GitLab's [Kubernetes cluster integration](../../clusters/index.md#installing-applications), it will [automatically be monitored](#about-managed-nginx-ingress-deployments) by Prometheus. + +For other deployments, there is [some configuration](#manually-setting-up-nginx-ingress-for-prometheus-monitoring) required depending on your installation: + +- NGINX Ingress should be version 0.9.0 or above, with metrics enabled. +- NGINX Ingress should be annotated for Prometheus monitoring. +- Prometheus should be configured to monitor annotated pods. + +### About managed NGINX Ingress deployments + +NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](../../clusters/index.md#getting-the-external-ip-address). + +NGINX is configured for Prometheus monitoring, by setting: + +- `enable-vts-status: "true"`, to export Prometheus metrics. +- `prometheus.io/scrape: "true"`, to enable automatic discovery. +- `prometheus.io/port: "10254"`, to specify the metrics port. + +When used in conjunction with the GitLab deployed Prometheus service, response metrics will be automatically collected. + +### Manually setting up NGINX Ingress for Prometheus monitoring + +Version 0.9.0 and above of [NGINX ingress](https://github.com/kubernetes/ingress-nginx) have built-in support for exporting Prometheus metrics. To enable, a ConfigMap setting must be passed: `enable-vts-status: "true"`. Once enabled, a Prometheus metrics endpoint will start running on port 10254. + +Next, the ingress needs to be annotated for Prometheus monitoring. Two new annotations need to be added: + +- `prometheus.io/scrape: "true"` +- `prometheus.io/port: "10254"` + +Managing these settings depends on how NGINX ingress has been deployed. If you have deployed via the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress), metrics can be enabled with `controller.stats.enabled` along with the required annotations. Alternatively it is possible edit the NGINX ingress YML directly in the [Kubernetes dashboard](https://github.com/kubernetes/dashboard). + +## Specifying the Environment label + +In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`. + +If you have used [Auto Deploy](../../../../topics/autodevops/index.md#auto-deploy) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part. diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index 2c2e8e2d556..2a490e3fd7f 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -77,7 +77,7 @@ GitLab users to the project. Once done, hit **Add users to project** and watch that there is a new member with the e-mail address we used above. From there on, you can resend the -invitation, change their access level or even delete them. +invitation, change their access level, or even delete them. ![Invite user members list](img/add_user_email_accept.png) diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md index 859ac92ef89..da6e6b5fd3a 100644 --- a/doc/user/project/merge_requests/allow_collaboration.md +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -1,20 +1,72 @@ # Allow collaboration on merge requests across forks -> [Introduced][ce-17395] in GitLab 10.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395) + in GitLab 10.6. + +When a user opens a merge request from a fork, they are given the option to allow +upstream members to collaborate with them on the source branch. This allows +the members of the upstream project to make small fixes or rebase branches +before merging, reducing the back and forth of accepting external contributions. This feature is available for merge requests across forked projects that are -publicly accessible. It makes it easier for members of projects to -collaborate on merge requests across forks. +publicly accessible. When enabled for a merge request, members with merge access to the target branch of the project will be granted write permissions to the source branch of the merge request. +## Enabling commit edits from upstream members + The feature can only be enabled by users who already have push access to the -source project, and only lasts while the merge request is open. +source project and only lasts while the merge request is open. Once enabled, +upstream members will also be able to retry the pipelines and jobs of the +merge request: + +1. Enable the contribution while creating or editing a merge request. + + ![Enable contribution](img/allow_collaboration.png) + +1. Once the merge request is created, you'll see that commits from members who + can merge to the target branch are allowed. + + ![Check that contribution is enabled](img/allow_collaboration_after_save.png) + +## Pushing to the fork as the upstream member + +If the creator of the merge request has enabled contributions from upstream +members, you can push directly to the branch of the forked repository. + +Assuming that: + +- The forked project URL is `git@gitlab.com:thedude/awesome-project.git`. +- The branch of the merge request is `update-docs`. + +Here's how the process would look like: + +1. First, you need to get the changes that the merge request has introduced. + Click the **Check out branch** button that has some pre-populated + commands that you can run. + + ![Check out branch button](img/checkout_button.png) + +1. Use the copy to clipboard button to copy the first command and paste them + in your terminal: + + ```sh + git fetch git@gitlab.com:thedude/awesome-project.git update-docs + git checkout -b thedude-awesome-project-update-docs FETCH_HEAD + ``` + + This will fetch the branch of the forked project and then create a local branch + based off the fetched branch. -Enable this functionality while creating or editing a merge request: +1. Make any changes you want and commit. +1. Push to the forked project: -![Enable collaboration](./img/allow_collaboration.png) + ```sh + git push git@gitlab.com:thedude/awesome-project.git thedude-awesome-project-update-docs:update-docs + ``` -[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395 + Note the colon (`:`) between the two branches. The above command will push the + local branch `thedude-awesome-project-update-docs` to the + `update-docs` branch of the `git@gitlab.com:thedude/awesome-project.git` repository. diff --git a/doc/user/project/merge_requests/img/allow_collaboration.png b/doc/user/project/merge_requests/img/allow_collaboration.png Binary files differindex 3c81e4c27b8..e40e8a6b11c 100644 --- a/doc/user/project/merge_requests/img/allow_collaboration.png +++ b/doc/user/project/merge_requests/img/allow_collaboration.png diff --git a/doc/user/project/merge_requests/img/allow_collaboration_after_save.png b/doc/user/project/merge_requests/img/allow_collaboration_after_save.png Binary files differnew file mode 100644 index 00000000000..4ba4c84c8c5 --- /dev/null +++ b/doc/user/project/merge_requests/img/allow_collaboration_after_save.png diff --git a/doc/user/project/merge_requests/img/checkout_button.png b/doc/user/project/merge_requests/img/checkout_button.png Binary files differnew file mode 100644 index 00000000000..9850795c9b4 --- /dev/null +++ b/doc/user/project/merge_requests/img/checkout_button.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index f479f9e4ef6..2c8a590fc45 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -92,6 +92,15 @@ request widget will show the "Removes source branch" text. ![Remove source branch status](img/remove_source_branch_status.png) +## Allow collaboration on merge requests across forks + +When a user opens a merge request from a fork, they are given the option to allow +upstream maintainers to collaborate with them on the source branch. This allows +the maintainers of the upstream project to make small fixes or rebase branches +before merging, reducing the back and forth of accepting community contributions. + +[Learn more about allowing upstream members to push to forks.](allow_collaboration.md) + ## Authorization for merge requests There are two main ways to have a merge request flow with GitLab: @@ -275,7 +284,11 @@ you can preview the changes submitted to a feature-branch through a merge reques in a per-branch basis. No need to checkout the branch, install and preview locally; all your changes will be available to preview by anyone with the Review Apps link. -[Read more about Review Apps.](../../../ci/review_apps/index.md) +With GitLab's [Route Maps](../../../ci/review_apps/index.md#route-maps) set, the +merge request widget takes you directly to the pages changed, making it easier and +faster to preview proposed modifications. + +[Read more about Review Apps](../../../ci/review_apps/index.md). ## Pipelines for merge requests diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md index 2b5abc7233f..c79fbad5765 100644 --- a/doc/user/project/operations/error_tracking.md +++ b/doc/user/project/operations/error_tracking.md @@ -1,6 +1,6 @@ # Error Tracking -> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.7. +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.8. Error tracking allows developers to easily discover and view the errors that their application may be generating. By surfacing error information where the code is being developed, efficiency and awareness can be increased. diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 290dfa5af84..595241b2cba 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -85,6 +85,12 @@ and a project within this group is called `blog`. Your project URL is `https://gitlab.com/websites/blog/`. Once you enable GitLab Pages for this project, the site will live under `https://websites.gitlab.io/blog/`. +- You created a group for your engineering department called `engineering`, +a subgroup for all your documentation websites called `docs`, +and a project within this subgroup is called `workflows`. Your project +URL is `https://gitlab.com/engineering/docs/workflows/`. Once you enable +GitLab Pages for this project, the site will live under +`https://engineering.gitlab.io/docs/workflows`. #### User and Group Websites @@ -97,9 +103,7 @@ will be published under `https://john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. ->**Note:** -GitLab Pages [does **not** support subgroups](../../group/subgroups/index.md#limitations). -You can only create the highest level group website. +> Support for subgroup project's websites was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8. **General example:** diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index ed049e2e648..a7846b1ee18 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -38,6 +38,7 @@ be served on. | Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | | Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | | Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| +| Project pages owned by a subgroup | `subgroup/projectname` | `http(s)://groupname.example.io/subgroup/projectname`| > **Warning:** > There are some known [limitations](#limitations) regarding namespaces served @@ -494,8 +495,8 @@ don't redirect HTTP to HTTPS. [rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" -GitLab Pages [does **not** support subgroups](../../group/subgroups/index.md#limitations). -You can only create the highest level group website. +GitLab Pages [does **not** support group websites for subgroups](../../group/subgroups/index.md#limitations). +You can only create the highest-level group website. ## Redirects in GitLab Pages diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 051277dfe02..ec8b8444d99 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -3,7 +3,7 @@ > **Notes**: > - This feature was introduced in 9.1 as [Trigger Schedule][ce-10533]. > - In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853]. -> - Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler). +> - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). Pipeline schedules can be used to run a pipeline at specific intervals, for example every month on the 22nd for a certain branch. diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 88d745b0ce4..bb9b4238ee9 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -1,7 +1,7 @@ # Pipelines settings To reach the pipelines settings navigate to your project's -**Settings ➔ CI/CD**. +**Settings > CI/CD**. The following settings can be configured per project. @@ -10,14 +10,14 @@ The following settings can be configured per project. With Git strategy, you can choose the default way your repository is fetched from GitLab in a job. -There are two options: +There are two options. Using: -- Using `git clone` which is slower since it clones the repository from scratch +- `git clone`, which is slower since it clones the repository from scratch for every job, ensuring that the project workspace is always pristine. -- Using `git fetch` which is faster as it re-uses the project workspace (falling +- `git fetch`, which is faster as it re-uses the project workspace (falling back to clone if it doesn't exist). -The default Git strategy can be overridden by the [GIT_STRATEGY variable][var] +The default Git strategy can be overridden by the [GIT_STRATEGY variable](../../../ci/yaml/README.md#git-strategy) in `.gitlab-ci.yml`. ## Timeout @@ -29,14 +29,14 @@ if the job surpasses the threshold, it is marked as failed. ### Timeout overriding on Runner level -> - [Introduced][ce-17221] in GitLab 10.7. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17221) in GitLab 10.7. Project defined timeout (either specific timeout set by user or the default -60 minutes timeout) may be [overridden on Runner level][timeout overriding]. +60 minutes timeout) may be [overridden on Runner level](../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner). ## Custom CI config path -> - [Introduced][ce-12509] in GitLab 9.4. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509) in GitLab 9.4. By default we look for the `.gitlab-ci.yml` file in the project's root directory. If you require a different location **within** the repository, @@ -59,7 +59,7 @@ job log using a regular expression. In the pipelines settings, search for the ![Pipelines settings test coverage](img/pipelines_settings_test_coverage.png) Leave blank if you want to disable it or enter a ruby regular expression. You -can use http://rubular.com to test your regex. +can use <http://rubular.com> to test your regex. If the pipeline succeeds, the coverage is shown in the merge request widget and in the jobs table. @@ -79,28 +79,28 @@ project setting under your project's **Settings > CI/CD > General pipelines sett If **Public pipelines** is enabled (default): -- for **public** projects, anyone can view the pipelines and access the job details - (output logs and artifacts) -- for **internal** projects, any logged in user can view the pipelines +- For **public** projects, anyone can view the pipelines and access the job details + (output logs and artifacts). +- For **internal** projects, any logged in user can view the pipelines and access the job details - (output logs and artifacts) -- for **private** projects, any member (guest or higher) can view the pipelines + (output logs and artifacts). +- For **private** projects, any member (guest or higher) can view the pipelines and access the job details - (output logs and artifacts) + (output logs and artifacts). If **Public pipelines** is disabled: -- for **public** projects, anyone can view the pipelines, but only members - (reporter or higher) can access the job details (output logs and artifacts) -- for **internal** projects, any logged in user can view the pipelines, - but only members (reporter or higher) can access the job details (output logs - and artifacts) -- for **private** projects, only members (reporter or higher) - can view the pipelines and access the job details (output logs and artifacts) +- For **public** projects, anyone can view the pipelines, but only members + (reporter or higher) can access the job details (output logs and artifacts). +- For **internal** projects, any logged in user can view the pipelines. + However, only members (reporter or higher) can access the job details (output logs + and artifacts). +- For **private** projects, only members (reporter or higher) + can view the pipelines and access the job details (output logs and artifacts). ## Auto-cancel pending pipelines -> [Introduced][ce-9362] in GitLab 9.1. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362) in GitLab 9.1. If you want to auto-cancel all pending non-HEAD pipelines on branch, when new pipeline will be created (after your git push or manually from UI), @@ -132,19 +132,19 @@ Depending on the status of your job, a badge can have the following values: You can access a pipeline status badge image using the following link: -``` +```text https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg ``` ### Test coverage report badge -GitLab makes it possible to define the regular expression for [coverage report], +GitLab makes it possible to define the regular expression for [coverage report](#test-coverage-parsing), that each job log will be matched against. This means that each job in the pipeline can have the test coverage percentage value defined. The test coverage badge can be accessed using following link: -``` +```text https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg ``` @@ -157,13 +157,28 @@ into your `README.md`: ![coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage) ``` -### Environment Variables +### Badge styles -[Environment variables](../../../ci/variables/README.html#variables) can be set in an environment to be available to a runner. +Pipeline badges can be rendered in different styles by adding the `style=style_name` parameter to the URL. Currently two styles are available: + +#### Flat (default) + +```text +https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat +``` + +![Badge flat style](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage&style=flat) -[var]: ../../../ci/yaml/README.md#git-strategy -[coverage report]: #test-coverage-parsing -[timeout overriding]: ../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner -[ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362 -[ce-12509]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509 -[ce-17221]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17221 +#### Flat square + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30120) in GitLab 11.8. + +```text +https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat-square +``` + +![Badge flat square style](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage&style=flat-square) + +## Environment Variables + +[Environment variables](../../../ci/variables/README.html#variables) can be set in an environment to be available to a runner. diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index 783081cec26..f05554ffc5b 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -2,16 +2,17 @@ Read through GiLab's branching documentation: -- [Create a branch](../web_editor.md#create-a-new-branch) -- [Default branch](#default-branch) -- [Protected branches](../../protected_branches.md#protected-branches) -- [Delete merged branches](#delete-merged-branches) -- [Branch filter search box](#branch-filter-search-box) +- [Create a branch](../web_editor.md#create-a-new-branch). +- [Default branch](#default-branch). +- [Protected branches](../../protected_branches.md#protected-branches). +- [Delete merged branches](#delete-merged-branches). +- [Branch filter search box](#branch-filter-search-box). See also: -- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow): use the best of GitLab for your branching strategies -- [Getting started with Git](../../../../topics/git/index.md) and GitLab +- [Branches API](../../../../api/branches.md), for information on operating on repository branches using the GitLab API. +- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow). Use the best of GitLab for your branching strategies. +- [Getting started with Git](../../../../topics/git/index.md) and GitLab. ## Default branch @@ -41,7 +42,6 @@ this operation. It's particularly useful to clean up old branches that were not deleted automatically when a merge request was merged. - ## Branch filter search box > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166) in GitLab 11.5. diff --git a/lib/api/api.rb b/lib/api/api.rb index 59b67c67f9d..a768b78cda5 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -110,6 +110,7 @@ module API mount ::API::GroupMilestones mount ::API::Groups mount ::API::GroupVariables + mount ::API::ImportGithub mount ::API::Internal mount ::API::Issues mount ::API::JobArtifacts diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a2a3c0a16d7..e0a48908122 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -115,6 +115,9 @@ module API expose :group_name do |group_link, options| group_link.group.name end + expose :group_full_path do |group_link, options| + group_link.group.full_path + end expose :group_access, as: :group_access_level expose :expires_at end @@ -277,7 +280,7 @@ module API # N+1 is solved then by using `subject.tags.map(&:name)` # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555 super(projects_relation).preload(:group) - .preload(project_group_links: :group, + .preload(project_group_links: { group: :route }, fork_network: :root_project, fork_network_member: :forked_from_project, forked_from_project: [:route, :forks, :tags, namespace: :route]) @@ -341,19 +344,23 @@ module API class GroupDetail < Group expose :projects, using: Entities::Project do |group, options| - GroupProjectsFinder.new( + projects = GroupProjectsFinder.new( group: group, current_user: options[:current_user], options: { only_owned: true } ).execute + + Entities::Project.prepare_relation(projects) end expose :shared_projects, using: Entities::Project do |group, options| - GroupProjectsFinder.new( + projects = GroupProjectsFinder.new( group: group, current_user: options[:current_user], options: { only_shared: true } ).execute + + Entities::Project.prepare_relation(projects) end end @@ -961,7 +968,7 @@ module API if options[:group_members] options[:group_members].find { |member| member.source_id == project.namespace_id } else - project.group.group_member(options[:current_user]) + project.group.highest_group_member(options[:current_user]) end end end diff --git a/lib/api/features.rb b/lib/api/features.rb index 1331248699f..835aac05905 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -16,15 +16,13 @@ module API end end - # rubocop: disable CodeReuse/ActiveRecord def gate_targets(params) - targets = [] - targets << Feature.group(params[:feature_group]) if params[:feature_group] - targets << UserFinder.new(params[:user]).find_by_username if params[:user] + Feature::Target.new(params).targets + end - targets + def gate_specified?(params) + Feature::Target.new(params).gate_specified? end - # rubocop: enable CodeReuse/ActiveRecord end resource :features do @@ -44,6 +42,7 @@ module API requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' optional :feature_group, type: String, desc: 'A Feature group name' optional :user, type: String, desc: 'A GitLab username' + optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' end post ':name' do feature = Feature.get(params[:name]) @@ -52,13 +51,13 @@ module API case value when true - if targets.present? + if gate_specified?(params) targets.each { |target| feature.enable(target) } else feature.enable end when false - if targets.present? + if gate_specified?(params) targets.each { |target| feature.disable(target) } else feature.disable diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb new file mode 100644 index 00000000000..bb4e536cf57 --- /dev/null +++ b/lib/api/import_github.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module API + class ImportGithub < Grape::API + rescue_from Octokit::Unauthorized, with: :provider_unauthorized + + helpers do + def client + @client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) + end + + def access_params + { github_access_token: params[:personal_access_token] } + end + + def client_options + {} + end + + def provider + :github + end + end + + desc 'Import a GitHub project' do + detail 'This feature was introduced in GitLab 11.3.4.' + success Entities::ProjectEntity + end + params do + requires :personal_access_token, type: String, desc: 'GitHub personal access token' + requires :repo_id, type: Integer, desc: 'GitHub repository ID' + optional :new_name, type: String, desc: 'New repo name' + requires :target_namespace, type: String, desc: 'Namespace to import repo into' + end + post 'import/github' do + result = Import::GithubService.new(client, current_user, params).execute(access_params, provider) + + if result[:status] == :success + present ProjectSerializer.new.represent(result[:project]) + else + status result[:http_status] + { errors: result[:message] } + end + end + end +end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f53ba0ab761..95371961398 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -35,7 +35,7 @@ module API end optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" - optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' + optional :default_branch_protection, type: Integer, values: Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' diff --git a/lib/api/tags.rb b/lib/api/tags.rb index aacdca3871a..f5359fd316c 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -20,12 +20,15 @@ module API desc: 'Return tags sorted in updated by `asc` or `desc` order.' optional :order_by, type: String, values: %w[name updated], default: 'updated', desc: 'Return tags ordered by `name` or `updated` fields.' + optional :search, type: String, desc: 'Return list of tags matching the search criteria' use :pagination end get ':id/repository/tags' do - tags = ::Kaminari.paginate_array(::TagsFinder.new(user_project.repository, sort: "#{params[:order_by]}_#{params[:sort]}").execute) + tags = ::TagsFinder.new(user_project.repository, + sort: "#{params[:order_by]}_#{params[:sort]}", + search: params[:search]).execute - present paginate(tags), with: Entities::Tag, project: user_project + present paginate(::Kaminari.paginate_array(tags)), with: Entities::Tag, project: user_project end desc 'Get a single repository tag' do diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 302b2797a34..ef0e3decc2c 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -22,7 +22,9 @@ module API end end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + WIKI_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(slug: API::NO_SLASH_URL_PART_REGEX) + + resource :projects, requirements: WIKI_ENDPOINT_REQUIREMENTS do desc 'Get a list of wiki pages' do success Entities::WikiPageBasic end @@ -103,7 +105,7 @@ module API requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded' optional :branch, type: String, desc: 'The name of the branch' end - post ":id/wikis/attachments", requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + post ":id/wikis/attachments" do authorize! :create_wiki, user_project result = ::Wikis::CreateAttachmentService.new(user_project, diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 0032ae8f84b..2bac84846c5 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -71,8 +71,14 @@ module Backup end def run_pipeline!(cmd_list, options = {}) - status_list = Open3.pipeline(*cmd_list, options) - raise Backup::Error, 'Backup failed' unless status_list.compact.all?(&:success?) + err_r, err_w = IO.pipe + options[:err] = err_w + status = Open3.pipeline(*cmd_list, options) + err_w.close + return if status.compact.all?(&:success?) + + regex = /^g?tar: \.: Cannot mkdir: No such file or directory$/ + raise Backup::Error, 'Backup failed' unless err_r.read =~ regex end end end diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb new file mode 100644 index 00000000000..97527976437 --- /dev/null +++ b/lib/banzai/filter/footnote_filter.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML Filter for footnotes + # + # Footnotes are supported in CommonMark. However we were stripping + # the ids during sanitization. Those are now allowed. + # + # Footnotes are numbered the same - the first one has `id=fn1`, the + # second is `id=fn2`, etc. In order to allow footnotes when rendering + # multiple markdown blocks on a page, we need to make each footnote + # reference unique. + # + # This filter adds a random number to each footnote (the same number + # can be used for a single render). So you get `id=fn1-4335` and `id=fn2-4335`. + # + class FootnoteFilter < HTML::Pipeline::Filter + INTEGER_PATTERN = /\A\d+\z/.freeze + FOOTNOTE_ID_PREFIX = 'fn'.freeze + FOOTNOTE_LINK_ID_PREFIX = 'fnref'.freeze + FOOTNOTE_LI_REFERENCE_PATTERN = /\A#{FOOTNOTE_ID_PREFIX}\d+\z/.freeze + FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze + FOOTNOTE_START_NUMBER = 1 + + def call + return doc unless first_footnote = doc.at_css("ol > li[id=#{fn_id(FOOTNOTE_START_NUMBER)}]") + + # Sanitization stripped off the section wrapper - add it back in + first_footnote.parent.wrap('<section class="footnotes">') + rand_suffix = "-#{random_number}" + + doc.css('sup > a[id]').each do |link_node| + ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX) + footnote_node = doc.at_css("li[id=#{fn_id(ref_num)}]") + backref_node = footnote_node.at_css("a[href=\"##{fnref_id(ref_num)}\"]") + + if ref_num =~ INTEGER_PATTERN && footnote_node && backref_node + link_node[:href] += rand_suffix + link_node[:id] += rand_suffix + footnote_node[:id] += rand_suffix + backref_node[:href] += rand_suffix + + # Sanitization stripped off class - add it back in + link_node.parent.append_class('footnote-ref') + backref_node.append_class('footnote-backref') + end + end + + doc + end + + private + + def random_number + @random_number ||= rand(10000) + end + + def fn_id(num) + "#{FOOTNOTE_ID_PREFIX}#{num}" + end + + def fnref_id(num) + "#{FOOTNOTE_LINK_ID_PREFIX}#{num}" + end + end + end +end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index c70c3f0c04e..fce042e8946 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -101,9 +101,9 @@ module Banzai def self_and_ancestors_ids(parent) if group_context?(parent) - parent.self_and_ancestors_ids + parent.self_and_ancestors.select(:id) elsif project_context?(parent) - parent.group&.self_and_ancestors_ids + parent.group&.self_and_ancestors&.select(:id) end end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 8ba09290e6d..edc053638a8 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -8,8 +8,8 @@ module Banzai class SanitizationFilter < HTML::Pipeline::SanitizationFilter include Gitlab::Utils::StrongMemoize - UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze - TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/ + UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze + TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/.freeze def whitelist strong_memoize(:whitelist) do @@ -45,10 +45,9 @@ module Banzai whitelist[:attributes][:all].delete('name') whitelist[:attributes]['a'].push('name') - # Allow any protocol in `a` elements... + # Allow any protocol in `a` elements + # and then remove links with unsafe protocols whitelist[:protocols].delete('a') - - # ...but then remove links with unsafe protocols whitelist[:transformers].push(self.class.remove_unsafe_links) # Remove `rel` attribute from `a` elements @@ -57,6 +56,12 @@ module Banzai # Remove any `style` properties not required for table alignment whitelist[:transformers].push(self.class.remove_unsafe_table_style) + # Allow `id` in a and li elements for footnotes + # and remove any `id` properties not matching for footnotes + whitelist[:attributes]['a'].push('id') + whitelist[:attributes]['li'] = %w(id) + whitelist[:transformers].push(self.class.remove_non_footnote_ids) + whitelist end @@ -112,6 +117,20 @@ module Banzai end end end + + def remove_non_footnote_ids + lambda do |env| + node = env[:node] + + return unless node.name == 'a' || node.name == 'li' + return unless node.has_attribute?('id') + + return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN + return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN + + node.remove_attribute('id') + end + end end end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5f13a6d6cde..d860dad0b6c 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -30,6 +30,7 @@ module Banzai Filter::AutolinkFilter, Filter::ExternalLinkFilter, Filter::SuggestionFilter, + Filter::FootnoteFilter, *reference_filters, diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb index 83e8808db07..6a608058813 100644 --- a/lib/bitbucket_server/client.rb +++ b/lib/bitbucket_server/client.rb @@ -4,18 +4,6 @@ module BitbucketServer class Client attr_reader :connection - ServerError = Class.new(StandardError) - - SERVER_ERRORS = [SocketError, - OpenSSL::SSL::SSLError, - Errno::ECONNRESET, - Errno::ECONNREFUSED, - Errno::EHOSTUNREACH, - Net::OpenTimeout, - Net::ReadTimeout, - Gitlab::HTTP::BlockedUrlError, - BitbucketServer::Connection::ConnectionError].freeze - def initialize(options = {}) @connection = Connection.new(options) end @@ -64,8 +52,6 @@ module BitbucketServer def get_collection(path, type, page_offset: 0, limit: nil) paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type, page_offset: page_offset, limit: limit) BitbucketServer::Collection.new(paginator) - rescue *SERVER_ERRORS => e - raise ServerError, e end end end diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb index 7efcdcf8619..9c14b26c65a 100644 --- a/lib/bitbucket_server/connection.rb +++ b/lib/bitbucket_server/connection.rb @@ -7,6 +7,17 @@ module BitbucketServer DEFAULT_API_VERSION = '1.0' SEPARATOR = '/' + NETWORK_ERRORS = [ + SocketError, + OpenSSL::SSL::SSLError, + Errno::ECONNRESET, + Errno::ECONNREFUSED, + Errno::EHOSTUNREACH, + Net::OpenTimeout, + Net::ReadTimeout, + Gitlab::HTTP::BlockedUrlError + ].freeze + attr_reader :api_version, :base_uri, :username, :token ConnectionError = Class.new(StandardError) @@ -27,6 +38,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end def post(path, body) @@ -38,6 +51,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end # We need to support two different APIs for deletion: @@ -55,6 +70,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end private diff --git a/lib/feature.rb b/lib/feature.rb index e048a443abc..e59cd70f822 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -102,4 +102,42 @@ class Feature expires_in: 1.hour) end end + + class Target + attr_reader :params + + def initialize(params) + @params = params + end + + def gate_specified? + %i(user project feature_group).any? { |key| params.key?(key) } + end + + def targets + [feature_group, user, project].compact + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def feature_group + return unless params.key?(:feature_group) + + Feature.group(params[:feature_group]) + end + # rubocop: enable CodeReuse/ActiveRecord + + def user + return unless params.key?(:user) + + UserFinder.new(params[:user]).find_by_username! + end + + def project + return unless params.key?(:project) + + Project.find_by_full_path(params[:project]) + end + end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 2ef54658a11..b91394f7f58 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -27,12 +27,52 @@ module Gitlab end end + def self.version_info + Gitlab::VersionInfo.parse(Gitlab::VERSION) + end + COM_URL = 'https://gitlab.com'.freeze APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} VERSION = File.read(root.join("VERSION")).strip.freeze INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze + def self.pre_release? + VERSION.include?('pre') + end + + def self.final_release? + !VERSION.include?('rc') && !pre_release? + end + + def self.minor_release + "#{version_info.major}.#{version_info.minor}" + end + + def self.prev_minor_release + "#{version_info.major}.#{version_info.minor - 1}" + end + + def self.prev_major_release + "#{version_info.major.to_i - 1}" + end + + def self.new_major_release? + version_info.minor.to_i.zero? + end + + def self.previous_release + if version_info.minor_version? + if version_info.patch_version? + minor_release + else + prev_minor_release + end + else + prev_major_release + end + end + def self.com? # Check `gl_subdomain?` as well to keep parity with gitlab.com Gitlab.config.gitlab.url == COM_URL || gl_subdomain? @@ -50,11 +90,11 @@ module Gitlab Rails.env.development? || org? || com? end - def self.pre_release? - VERSION.include?('pre') - end + def self.process_name + return 'sidekiq' if Sidekiq.server? + return 'console' if defined?(Rails::Console) + return 'test' if Rails.env.test? - def self.version_info - Gitlab::VersionInfo.parse(Gitlab::VERSION) + 'web' end end diff --git a/lib/gitlab/access/branch_protection.rb b/lib/gitlab/access/branch_protection.rb new file mode 100644 index 00000000000..f039e5c011f --- /dev/null +++ b/lib/gitlab/access/branch_protection.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Access + # A wrapper around Integer based branch protection levels. + # + # This wrapper can be used to work with branch protection levels without + # having to directly refer to the constants. For example, instead of this: + # + # if access_level == Gitlab::Access::PROTECTION_DEV_CAN_PUSH + # ... + # end + # + # You can write this instead: + # + # protection = BranchProtection.new(access_level) + # + # if protection.developer_can_push? + # ... + # end + class BranchProtection + attr_reader :level + + # @param [Integer] level The branch protection level as an Integer. + def initialize(level) + @level = level + end + + def any? + level != PROTECTION_NONE + end + + def developer_can_push? + level == PROTECTION_DEV_CAN_PUSH + end + + def developer_can_merge? + level == PROTECTION_DEV_CAN_MERGE + end + end + end +end diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb index a0244a3cea1..48d134f91b0 100644 --- a/lib/gitlab/auth/ldap/person.rb +++ b/lib/gitlab/auth/ldap/person.rb @@ -98,9 +98,7 @@ module Gitlab private - def entry - @entry - end + attr_reader :entry def config @config ||= Gitlab::Auth::LDAP::Config.new(provider) diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb index aaf520d70f6..c8d83cc1803 100644 --- a/lib/gitlab/background_migration/backfill_project_repositories.rb +++ b/lib/gitlab/background_migration/backfill_project_repositories.rb @@ -83,7 +83,7 @@ module Gitlab extend ActiveSupport::Concern def full_path - @full_path ||= build_full_path + route&.path || build_full_path end def build_full_path @@ -99,7 +99,12 @@ module Gitlab end end - # Namespace model. + # Route model + class Route < ActiveRecord::Base + belongs_to :source, inverse_of: :route, polymorphic: true + end + + # Namespace model class Namespace < ActiveRecord::Base self.table_name = 'namespaces' self.inheritance_column = nil @@ -108,6 +113,8 @@ module Gitlab belongs_to :parent, class_name: 'Namespace', inverse_of: 'namespaces' + has_one :route, -> { where(source_type: 'Namespace') }, inverse_of: :source, foreign_key: :source_id + has_many :projects, inverse_of: :parent has_many :namespaces, inverse_of: :parent end @@ -134,6 +141,7 @@ module Gitlab belongs_to :parent, class_name: 'Namespace', foreign_key: :namespace_id, inverse_of: 'projects' + has_one :route, -> { where(source_type: 'Project') }, inverse_of: :source, foreign_key: :source_id has_one :project_repository, inverse_of: :project delegate :disk_path, to: :storage @@ -194,6 +202,8 @@ module Gitlab def project_repositories(start_id, stop_id) projects .without_project_repository + .includes(:route, parent: [:route]).references(:routes) + .includes(:parent).references(:namespaces) .where(id: start_id..stop_id) .map { |project| build_attributes_for_project(project) } .compact diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 3239743bfff..1d8904f7b29 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -16,13 +16,6 @@ module Gitlab dependencies before_script after_script variables environment coverage retry parallel extends].freeze - DEFAULT_ONLY_POLICY = { - refs: %w(branches tags) - }.freeze - - DEFAULT_EXCEPT_POLICY = { - }.freeze - validations do validates :config, allowed_keys: ALLOWED_KEYS validates :config, presence: true @@ -73,7 +66,8 @@ module Gitlab description: 'Services that will be used to execute this job.' entry :only, Entry::Policy, - description: 'Refs policy this job will be executed for.' + description: 'Refs policy this job will be executed for.', + default: { refs: %w[branches tags] } entry :except, Entry::Policy, description: 'Refs policy this job will be executed for.' @@ -156,8 +150,8 @@ module Gitlab services: services_value, stage: stage_value, cache: cache_value, - only: DEFAULT_ONLY_POLICY.deep_merge(only_value.to_h), - except: DEFAULT_EXCEPT_POLICY.deep_merge(except_value.to_h), + only: only_value, + except: except_value, variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 998da1f6837..9c677bf6617 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -64,7 +64,8 @@ module Gitlab end end - def self.default + def value + default.to_h.deep_merge(subject.value.to_h) end end end diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb index eaf8b38aa3c..e9cbcb31e21 100644 --- a/lib/gitlab/ci/config/entry/retry.rb +++ b/lib/gitlab/ci/config/entry/retry.rb @@ -82,9 +82,6 @@ module Gitlab 'retry config' end end - - def self.default - end end end end diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index 89d790ebfa6..c9d0c7cb568 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -14,7 +14,7 @@ module Gitlab validates :config, variables: true end - def self.default + def self.default(**) {} end diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb index b1db9084662..94f4a4e36c9 100644 --- a/lib/gitlab/ci/cron_parser.rb +++ b/lib/gitlab/ci/cron_parser.rb @@ -35,7 +35,7 @@ module Gitlab # NOTE: # cron_timezone can only accept timezones listed in TZInfo::Timezone. # Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted, - # because Rufus::Scheduler only supports TZInfo::Timezone. + # because Fugit::Cron only supports TZInfo::Timezone. # # For example, those codes have the same effect. # Time.zone = 'Pacific Time (US & Canada)' (ActiveSupport::TimeZone) @@ -47,10 +47,7 @@ module Gitlab # If you want to know more, please take a look # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb def try_parse_cron(cron, cron_timezone) - cron_line = Rufus::Scheduler.parse("#{cron} #{cron_timezone}") - cron_line if cron_line.is_a?(Rufus::Scheduler::CronLine) - rescue - # noop + Fugit::Cron.parse("#{cron} #{cron_timezone}") end end end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index e292641da2b..95160e1432f 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -49,7 +49,7 @@ variables: POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG - KUBERNETES_VERSION: 1.10.9 + KUBERNETES_VERSION: 1.11.6 HELM_VERSION: 2.11.0 DOCKER_DRIVER: overlay2 @@ -667,6 +667,14 @@ rollout 100%: create_application_secret "$track" + env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]') + eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS + if [ -n "$env_ADDITIONAL_HOSTS" ]; then + additional_hosts="{$env_ADDITIONAL_HOSTS}" + elif [ -n "$ADDITIONAL_HOSTS" ]; then + additional_hosts="{$ADDITIONAL_HOSTS}" + fi + if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then echo "Deploying first release with database initialization..." helm upgrade --install \ @@ -681,6 +689,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ --set postgresql.enabled="$postgres_enabled" \ --set postgresql.nameOverride="postgres" \ @@ -714,6 +723,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ --set postgresql.enabled="$postgres_enabled" \ --set postgresql.nameOverride="postgres" \ diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb index afdb60b2cd5..37ba16dba25 100644 --- a/lib/gitlab/config/entry/configurable.rb +++ b/lib/gitlab/config/entry/configurable.rb @@ -56,6 +56,7 @@ module Gitlab def entry(key, entry, metadata) factory = ::Gitlab::Config::Entry::Factory.new(entry) .with(description: metadata[:description]) + .with(default: metadata[:default]) (@nodes ||= {}).merge!(key.to_sym => factory) end diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb index 30d43c9f9a1..79f9ff32514 100644 --- a/lib/gitlab/config/entry/factory.rb +++ b/lib/gitlab/config/entry/factory.rb @@ -12,7 +12,7 @@ module Gitlab def initialize(entry) @entry = entry @metadata = {} - @attributes = {} + @attributes = { default: entry.default } end def value(value) @@ -21,12 +21,12 @@ module Gitlab end def metadata(metadata) - @metadata.merge!(metadata) + @metadata.merge!(metadata.compact) self end def with(attributes) - @attributes.merge!(attributes) + @attributes.merge!(attributes.compact) self end @@ -38,9 +38,7 @@ module Gitlab # See issue #18775. # if @value.nil? - Entry::Unspecified.new( - fabricate_unspecified - ) + Entry::Unspecified.new(fabricate_unspecified) else fabricate(@entry, @value) end @@ -53,10 +51,12 @@ module Gitlab # If entry has a default value we fabricate concrete node # with default value. # - if @entry.default.nil? + default = @attributes.fetch(:default) + + if default.nil? fabricate(Entry::Undefined) else - fabricate(@entry, @entry.default) + fabricate(@entry, default) end end @@ -64,6 +64,7 @@ module Gitlab entry.new(value, @metadata).tap do |node| node.key = @attributes[:key] node.parent = @attributes[:parent] + node.default = @attributes[:default] node.description = @attributes[:description] end end diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb index 30357b2c95b..9999ab4ff95 100644 --- a/lib/gitlab/config/entry/node.rb +++ b/lib/gitlab/config/entry/node.rb @@ -10,7 +10,7 @@ module Gitlab InvalidError = Class.new(StandardError) attr_reader :config, :metadata - attr_accessor :key, :parent, :description + attr_accessor :key, :parent, :default, :description def initialize(config, **metadata) @config = config @@ -85,7 +85,7 @@ module Gitlab "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" end - def self.default + def self.default(**) end def self.aspects diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb index 3e148fe2e91..5fbf7565e2a 100644 --- a/lib/gitlab/config/entry/simplifiable.rb +++ b/lib/gitlab/config/entry/simplifiable.rb @@ -6,6 +6,8 @@ module Gitlab class Simplifiable < SimpleDelegator EntryStrategy = Struct.new(:name, :condition) + attr_reader :subject + def initialize(config, **metadata) unless self.class.const_defined?(:UnknownStrategy) raise ArgumentError, 'UndefinedStrategy not available!' @@ -17,7 +19,7 @@ module Gitlab entry = self.class.entry_class(strategy) - super(entry.new(config, metadata)) + super(@subject = entry.new(config, metadata)) end def self.strategy(name, **opts) @@ -37,6 +39,9 @@ module Gitlab self::UnknownStrategy end end + + def self.default + end end end end diff --git a/lib/gitlab/git/bundle_file.rb b/lib/gitlab/git/bundle_file.rb new file mode 100644 index 00000000000..8384a436fcc --- /dev/null +++ b/lib/gitlab/git/bundle_file.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class BundleFile + # All git bundle files start with this string + # + # https://github.com/git/git/blob/v2.20.1/bundle.c#L15 + MAGIC = "# v2 git bundle\n" + + InvalidBundleError = Class.new(StandardError) + + attr_reader :filename + + def self.check!(filename) + new(filename).check! + end + + def initialize(filename) + @filename = filename + end + + def check! + data = File.open(filename, 'r') { |f| f.read(MAGIC.size) } + + raise InvalidBundleError, 'Invalid bundle file' unless data == MAGIC + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 5bbedc9d5e3..786c90f9272 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -789,6 +789,11 @@ module Gitlab end def create_from_bundle(bundle_path) + # It's important to check that the linked-to file is actually a valid + # .bundle file as it is passed to `git clone`, which may otherwise + # interpret it as a pointer to another repository + ::Gitlab::Git::BundleFile.check!(bundle_path) + gitaly_repository_client.create_from_bundle(bundle_path) end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 15137140639..9b1794eec91 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,10 +8,7 @@ module Gitlab def add_gon_variables gon.api_version = 'v4' - gon.default_avatar_url = - Gitlab::Utils.append_path( - Gitlab.config.gitlab.url, - ActionController::Base.helpers.image_path('no_avatar.png')) + gon.default_avatar_url = default_avatar_url gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size gon.asset_host = ActionController::Base.asset_host gon.webpack_public_path = webpack_public_path @@ -50,5 +47,15 @@ module Gitlab # use this method to push multiple feature flags. gon.push({ features: { var_name => enabled } }, true) end + + def default_avatar_url + # We can't use ActionController::Base.helpers.image_url because it + # doesn't return an actual URL because request is nil for some reason. + # + # We also can't use Gitlab::Utils.append_path because the image path + # may be an absolute URL. + URI.join(Gitlab.config.gitlab.url, + ActionController::Base.helpers.image_path('no_avatar.png')).to_s + end end end diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index f142f9da43d..817db12ac55 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -71,12 +71,16 @@ module Gitlab @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {} end + def relative_url + File.join('', Gitlab.config.gitlab.relative_url_root).chomp('/') + end + # Overridden in EE module def whitelisted_routes - grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route + grack_route? || internal_route? || lfs_route? || sidekiq_route? end - def grack_route + def grack_route? # Calling route_hash may be expensive. Only do it if we think there's a possible match return false unless request.path.end_with?('.git/git-upload-pack', '.git/git-receive-pack') @@ -84,7 +88,11 @@ module Gitlab WHITELISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end - def lfs_route + def internal_route? + ReadOnly.internal_routes.any? { |path| request.path.include?(path) } + end + + def lfs_route? # Calling route_hash may be expensive. Only do it if we think there's a possible match unless request.path.end_with?('/info/lfs/objects/batch', '/info/lfs/locks', '/info/lfs/locks/verify') || @@ -95,8 +103,8 @@ module Gitlab WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end - def sidekiq_route - request.path.start_with?('/admin/sidekiq') + def sidekiq_route? + request.path.start_with?("#{relative_url}/admin/sidekiq") end end end diff --git a/lib/gitlab/release_blog_post.rb b/lib/gitlab/release_blog_post.rb new file mode 100644 index 00000000000..639aee61464 --- /dev/null +++ b/lib/gitlab/release_blog_post.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +require 'singleton' + +module Gitlab + class ReleaseBlogPost + include Singleton + + RELEASE_RSS_URL = 'https://about.gitlab.com/releases.xml' + + def blog_post_url + @url ||= fetch_blog_post_url + end + + private + + def fetch_blog_post_url + installed_version = Gitlab.final_release? ? Gitlab.minor_release : Gitlab.previous_release + response = Gitlab::HTTP.get(RELEASE_RSS_URL, verify: false) + + return unless response.code == 200 + + blog_entry = find_installed_blog_entry(response, installed_version) + blog_entry['id'] if blog_entry + end + + def find_installed_blog_entry(response, installed_version) + response['feed']['entry'].find do |entry| + entry['release'] == installed_version || matches_previous_release_post(entry['release'], installed_version) + end + end + + def should_match_previous_release_post? + Gitlab.new_major_release? && !Gitlab.final_release? + end + + def matches_previous_release_post(rss_release_version, installed_version) + should_match_previous_release_post? && rss_release_version[/\d+/] == installed_version + end + end +end diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb index 46d01964eac..956c16117f5 100644 --- a/lib/gitlab/sentry.rb +++ b/lib/gitlab/sentry.rb @@ -52,14 +52,6 @@ module Gitlab end end - def self.program_context - if Sidekiq.server? - 'sidekiq' - else - 'rails' - end - end - def self.should_raise_for_dev? Rails.env.development? || Rails.env.test? end diff --git a/lib/gitlab/tracing.rb b/lib/gitlab/tracing.rb new file mode 100644 index 00000000000..3c4db42ac06 --- /dev/null +++ b/lib/gitlab/tracing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + # Only enable tracing when the `GITLAB_TRACING` env var is configured. Note that we avoid using ApplicationSettings since + # the same environment variable needs to be configured for Workhorse, Gitaly and any other components which + # emit tracing. Since other components may start before Rails, and may not have access to ApplicationSettings, + # an env var makes more sense. + def self.enabled? + connection_string.present? + end + + def self.connection_string + ENV['GITLAB_TRACING'] + end + end +end diff --git a/lib/gitlab/tracing/factory.rb b/lib/gitlab/tracing/factory.rb new file mode 100644 index 00000000000..fc714164353 --- /dev/null +++ b/lib/gitlab/tracing/factory.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "cgi" + +module Gitlab + module Tracing + class Factory + OPENTRACING_SCHEME = "opentracing" + + def self.create_tracer(service_name, connection_string) + return unless connection_string.present? + + begin + opentracing_details = parse_connection_string(connection_string) + driver_name = opentracing_details[:driver_name] + + case driver_name + when "jaeger" + JaegerFactory.create_tracer(service_name, opentracing_details[:options]) + else + raise "Unknown driver: #{driver_name}" + end + rescue => e + # Can't create the tracer? Warn and continue sans tracer + warn "Unable to instantiate tracer: #{e}" + nil + end + end + + def self.parse_connection_string(connection_string) + parsed = URI.parse(connection_string) + + unless valid_uri?(parsed) + raise "Invalid tracing connection string" + end + + { + driver_name: parsed.host, + options: parse_query(parsed.query) + } + end + private_class_method :parse_connection_string + + def self.parse_query(query) + return {} unless query + + CGI.parse(query).symbolize_keys.transform_values(&:first) + end + private_class_method :parse_query + + def self.valid_uri?(uri) + return false unless uri + + uri.scheme == OPENTRACING_SCHEME && + uri.host.to_s =~ /^[a-z0-9_]+$/ && + uri.path.empty? + end + private_class_method :valid_uri? + end + end +end diff --git a/lib/gitlab/tracing/jaeger_factory.rb b/lib/gitlab/tracing/jaeger_factory.rb new file mode 100644 index 00000000000..2682007302a --- /dev/null +++ b/lib/gitlab/tracing/jaeger_factory.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'jaeger/client' + +module Gitlab + module Tracing + class JaegerFactory + # When the probabilistic sampler is used, by default 0.1% of requests will be traced + DEFAULT_PROBABILISTIC_RATE = 0.001 + + # The default port for the Jaeger agent UDP listener + DEFAULT_UDP_PORT = 6831 + + # Reduce this from default of 10 seconds as the Ruby jaeger + # client doesn't have overflow control, leading to very large + # messages which fail to send over UDP (max packet = 64k) + # Flush more often, with smaller packets + FLUSH_INTERVAL = 5 + + def self.create_tracer(service_name, options) + kwargs = { + service_name: service_name, + sampler: get_sampler(options[:sampler], options[:sampler_param]), + reporter: get_reporter(service_name, options[:http_endpoint], options[:udp_endpoint]) + }.compact + + extra_params = options.except(:sampler, :sampler_param, :http_endpoint, :udp_endpoint, :strict_parsing, :debug) # rubocop: disable CodeReuse/ActiveRecord + if extra_params.present? + message = "jaeger tracer: invalid option: #{extra_params.keys.join(", ")}" + + if options[:strict_parsing] + raise message + else + warn message + end + end + + Jaeger::Client.build(kwargs) + end + + def self.get_sampler(sampler_type, sampler_param) + case sampler_type + when "probabilistic" + sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE + Jaeger::Samplers::Probabilistic.new(rate: sampler_rate) + when "const" + const_value = sampler_param == "1" + Jaeger::Samplers::Const.new(const_value) + else + nil + end + end + private_class_method :get_sampler + + def self.get_reporter(service_name, http_endpoint, udp_endpoint) + encoder = Jaeger::Encoders::ThriftEncoder.new(service_name: service_name) + + if http_endpoint.present? + sender = get_http_sender(encoder, http_endpoint) + elsif udp_endpoint.present? + sender = get_udp_sender(encoder, udp_endpoint) + else + return nil + end + + Jaeger::Reporters::RemoteReporter.new( + sender: sender, + flush_interval: FLUSH_INTERVAL + ) + end + private_class_method :get_reporter + + def self.get_http_sender(encoder, address) + Jaeger::HttpSender.new( + url: address, + encoder: encoder, + logger: Logger.new(STDOUT) + ) + end + private_class_method :get_http_sender + + def self.get_udp_sender(encoder, address) + pair = address.split(":", 2) + host = pair[0] + port = pair[1] ? pair[1].to_i : DEFAULT_UDP_PORT + + Jaeger::UdpSender.new( + host: host, + port: port, + encoder: encoder, + logger: Logger.new(STDOUT) + ) + end + private_class_method :get_udp_sender + end + end +end diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb index aa6d5310161..142ead12c08 100644 --- a/lib/gitlab/version_info.rb +++ b/lib/gitlab/version_info.rb @@ -20,6 +20,14 @@ module Gitlab @patch = patch end + def minor_version? + minor.to_i > 0 + end + + def patch_version? + patch.to_i > 0 + end + def <=>(other) return unless other.is_a? VersionInfo return unless valid? && other.valid? diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb index e06245294c4..46aad8aa885 100644 --- a/lib/system_check/base_check.rb +++ b/lib/system_check/base_check.rb @@ -70,18 +70,14 @@ module SystemCheck # multiple reasons why a check can fail # # @param [String] reason to be displayed - def skip_reason=(reason) - @skip_reason = reason - end + attr_writer :skip_reason # Skip reason defined during runtime # # This value have precedence over the one defined in the subclass # # @return [String] the reason - def skip_reason - @skip_reason - end + attr_reader :skip_reason # Does the check support automatically repair routine? # diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e7aa7e453bd..795a46382a7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -129,6 +129,9 @@ msgstr "" msgid "%{issuableType} will be removed! Are you sure?" msgstr "" +msgid "%{link_start}Read more%{link_end} about role permissions" +msgstr "" + msgid "%{loadingIcon} Started" msgstr "" @@ -399,6 +402,9 @@ msgstr "" msgid "Add reaction" msgstr "" +msgid "Add to project" +msgstr "" + msgid "Add todo" msgstr "" @@ -1305,6 +1311,9 @@ msgstr "" msgid "Choose a file" msgstr "" +msgid "Choose a role permission" +msgstr "" + msgid "Choose a template..." msgstr "" @@ -1464,6 +1473,9 @@ msgstr "" msgid "Closed" msgstr "" +msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}" +msgstr "" + msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" @@ -1890,6 +1902,9 @@ msgstr "" msgid "ClusterIntegration|sign up" msgstr "" +msgid "Code" +msgstr "" + msgid "Cohorts" msgstr "" @@ -1928,9 +1943,15 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit %{commit_id}" +msgstr "" + msgid "Commit Message" msgstr "" +msgid "Commit deleted" +msgstr "" + msgid "Commit duration in minutes for last 30 commits" msgstr "" @@ -2770,6 +2791,9 @@ msgstr "" msgid "Enable the Performance Bar for a given group." msgstr "" +msgid "Enable two-factor authentication" +msgstr "" + msgid "Enable usage ping" msgstr "" @@ -2947,6 +2971,9 @@ msgstr "" msgid "Error occurred when toggling the notification subscription" msgstr "" +msgid "Error rendering markdown preview" +msgstr "" + msgid "Error saving label update." msgstr "" @@ -3010,6 +3037,9 @@ msgstr "" msgid "Existing folder" msgstr "" +msgid "Existing members and groups" +msgstr "" + msgid "Expand" msgstr "" @@ -3136,6 +3166,12 @@ msgstr "" msgid "Filter by two-factor authentication" msgstr "" +msgid "Filter results by group" +msgstr "" + +msgid "Filter results by project" +msgstr "" + msgid "Filter..." msgstr "" @@ -3145,6 +3181,9 @@ msgstr "" msgid "Find by path" msgstr "" +msgid "Find existing members by name" +msgstr "" + msgid "Find file" msgstr "" @@ -3397,6 +3436,9 @@ msgstr "" msgid "Group name" msgstr "" +msgid "Group:" +msgstr "" + msgid "Group: %{group_name}" msgstr "" @@ -3436,6 +3478,9 @@ msgstr "" msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgstr "" +msgid "Groups with access to <strong>%{project_name}</strong>" +msgstr "" + msgid "GroupsDropdown|Frequently visited" msgstr "" @@ -3663,12 +3708,21 @@ msgstr "" msgid "Import issues" msgstr "" +msgid "Import members" +msgstr "" + +msgid "Import members from another project" +msgstr "" + msgid "Import multiple repositories by uploading a manifest file." msgstr "" msgid "Import project" msgstr "" +msgid "Import project members" +msgstr "" + msgid "Import projects from Bitbucket" msgstr "" @@ -3774,6 +3828,12 @@ msgstr "" msgid "Invite" msgstr "" +msgid "Invite group" +msgstr "" + +msgid "Invite member" +msgstr "" + msgid "Invoke Count" msgstr "" @@ -4114,6 +4174,9 @@ msgstr "" msgid "Manage project labels" msgstr "" +msgid "Manage two-factor authentication" +msgstr "" + msgid "Manifest" msgstr "" @@ -4195,6 +4258,12 @@ msgstr "" msgid "Members" msgstr "" +msgid "Members can be added by project <i>Maintainers</i> or <i>Owners</i>" +msgstr "" + +msgid "Members of <strong>%{project_name}</strong>" +msgstr "" + msgid "Merge Request" msgstr "" @@ -4742,6 +4811,9 @@ msgstr "" msgid "Only project members can comment." msgstr "" +msgid "Only project members will be imported. Group members will be skipped." +msgstr "" + msgid "Oops, are you sure?" msgstr "" @@ -4865,6 +4937,9 @@ msgstr "" msgid "Personal Access Token" msgstr "" +msgid "Personal project creation is not allowed. Please contact your administrator with questions" +msgstr "" + msgid "Pick a name" msgstr "" @@ -5144,6 +5219,12 @@ msgstr "" msgid "Profiles|Account scheduled for removal." msgstr "" +msgid "Profiles|Activate signin with one of the following services" +msgstr "" + +msgid "Profiles|Active" +msgstr "" + msgid "Profiles|Add key" msgstr "" @@ -5159,6 +5240,9 @@ msgstr "" msgid "Profiles|Change username" msgstr "" +msgid "Profiles|Changing your username can have unintended side effects." +msgstr "" + msgid "Profiles|Choose file..." msgstr "" @@ -5171,6 +5255,15 @@ msgstr "" msgid "Profiles|Clear status" msgstr "" +msgid "Profiles|Click on icon to activate signin with one of the following services" +msgstr "" + +msgid "Profiles|Connect" +msgstr "" + +msgid "Profiles|Connected Accounts" +msgstr "" + msgid "Profiles|Current path: %{path}" msgstr "" @@ -5189,6 +5282,9 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" +msgid "Profiles|Disconnect" +msgstr "" + msgid "Profiles|Do not show on profile" msgstr "" @@ -5201,6 +5297,9 @@ msgstr "" msgid "Profiles|Enter your name, so people you know can recognize you" msgstr "" +msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)" +msgstr "" + msgid "Profiles|Invalid password" msgstr "" @@ -5237,6 +5336,9 @@ msgstr "" msgid "Profiles|Set new profile picture" msgstr "" +msgid "Profiles|Social sign-in" +msgstr "" + msgid "Profiles|Some options are unavailable for LDAP accounts" msgstr "" @@ -5264,6 +5366,9 @@ msgstr "" msgid "Profiles|This information will appear on your profile" msgstr "" +msgid "Profiles|Two-Factor Authentication" +msgstr "" + msgid "Profiles|Type your %{confirmationValue} to confirm:" msgstr "" @@ -5396,12 +5501,18 @@ msgstr "" msgid "Project export started. A download link will be sent by email." msgstr "" +msgid "Project members" +msgstr "" + msgid "Project name" msgstr "" msgid "Project slug" msgstr "" +msgid "Project:" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -5964,6 +6075,9 @@ msgstr "" msgid "Search for projects, issues, etc." msgstr "" +msgid "Search groups" +msgstr "" + msgid "Search merge requests" msgstr "" @@ -5979,6 +6093,9 @@ msgstr "" msgid "Search project" msgstr "" +msgid "Search projects" +msgstr "" + msgid "Search users" msgstr "" @@ -6039,6 +6156,9 @@ msgstr "" msgid "Select branch/tag" msgstr "" +msgid "Select members to invite" +msgstr "" + msgid "Select project" msgstr "" @@ -6272,6 +6392,9 @@ msgstr "" msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job" msgstr "" +msgid "Snippet Contents" +msgstr "" + msgid "Snippets" msgstr "" @@ -7218,6 +7341,9 @@ msgstr "" msgid "Title" msgstr "" +msgid "Titles and Filenames" +msgstr "" + msgid "To GitLab" msgstr "" @@ -7641,6 +7767,9 @@ msgstr "" msgid "Want to see the data? Please ask an administrator for access." msgstr "" +msgid "We couldn't find any results matching" +msgstr "" + msgid "We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed." msgstr "" @@ -7659,6 +7788,9 @@ msgstr "" msgid "Web terminal" msgstr "" +msgid "What's new?" +msgstr "" + msgid "When a runner is locked, it cannot be assigned to other projects" msgstr "" @@ -7863,6 +7995,9 @@ msgstr "" msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}" msgstr "" +msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group." +msgstr "" + msgid "You can move around the graph by using the arrow keys." msgstr "" @@ -8016,6 +8151,9 @@ msgstr "" msgid "Your name" msgstr "" +msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it" +msgstr "" + msgid "Your projects" msgstr "" @@ -8040,6 +8178,9 @@ msgstr "" msgid "command line instructions" msgstr "" +msgid "commented on %{link_to_project}" +msgstr "" + msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue." msgstr "" @@ -8113,6 +8254,12 @@ msgstr "" msgid "importing" msgstr "" +msgid "in group %{link_to_group}" +msgstr "" + +msgid "in project %{link_to_project}" +msgstr "" + msgid "issue boards" msgstr "" @@ -8365,6 +8512,9 @@ msgstr "" msgid "personal access token" msgstr "" +msgid "private" +msgstr "" + msgid "project" msgstr "" diff --git a/package.json b/package.json index 325341f1ca7..1fd7e53d62c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .", "jest": "BABEL_ENV=jest jest", + "jsdoc": "jsdoc -c config/jsdocs.config.js", "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js", "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js", "karma-start": "BABEL_ENV=karma karma start config/karma.config.js", @@ -75,7 +76,7 @@ "js-cookie": "^2.1.3", "jszip": "^3.1.3", "jszip-utils": "^0.0.2", - "katex": "^0.9.0", + "katex": "^0.10.0", "marked": "^0.3.12", "mermaid": "^8.0.0-rc.8", "monaco-editor": "^0.14.3", @@ -132,6 +133,7 @@ "babel-types": "^6.26.0", "chalk": "^2.4.1", "commander": "^2.18.0", + "docdash": "^1.0.2", "eslint": "~5.9.0", "eslint-import-resolver-jest": "^2.1.1", "eslint-import-resolver-webpack": "^0.10.1", @@ -148,6 +150,8 @@ "jasmine-jquery": "^2.1.1", "jest": "^23.6.0", "jest-junit": "^5.2.0", + "jsdoc": "^3.5.5", + "jsdoc-vue": "^1.0.0", "karma": "^3.0.0", "karma-chrome-launcher": "^2.2.0", "karma-coverage-istanbul-reporter": "^2.0.4", @@ -157,12 +161,13 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0-beta.0", "nodemon": "^1.18.4", - "prettier": "1.15.2", + "prettier": "1.15.3", "vue-jest": "^3.0.2", "webpack-dev-server": "^3.1.14", "yarn-deduplicate": "^1.0.5" }, "engines": { + "node": ">=8.10.0", "yarn": "^1.10.0" } } diff --git a/qa/Gemfile b/qa/Gemfile index 75ad7bd07af..873eac1013f 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -7,4 +7,4 @@ gem 'rake', '~> 12.3.0' gem 'rspec', '~> 3.7' gem 'selenium-webdriver', '~> 3.12' gem 'airborne', '~> 0.2.13' -gem 'nokogiri', '~> 1.8.5' +gem 'nokogiri', '~> 1.10.1' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 55f3211482b..419cacdb2af 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -44,11 +44,11 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_mime (1.0.0) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.1) netrc (0.11.0) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -97,7 +97,7 @@ DEPENDENCIES airborne (~> 0.2.13) capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) - nokogiri (~> 1.8.5) + nokogiri (~> 1.10.0) pry-byebug (~> 3.5.1) rake (~> 12.3.0) rspec (~> 3.7) diff --git a/qa/Rakefile b/qa/Rakefile new file mode 100644 index 00000000000..8df1cfdc174 --- /dev/null +++ b/qa/Rakefile @@ -0,0 +1,6 @@ +require_relative 'qa/tools/revoke_all_personal_access_tokens' + +desc "Revokes all personal access tokens" +task :revoke_personal_access_tokens do + QA::Tools::RevokeAllPersonalAccessTokens.new.run +end @@ -158,6 +158,10 @@ module QA autoload :Activity, 'qa/page/project/activity' autoload :Menu, 'qa/page/project/menu' + module Branches + autoload :Show, 'qa/page/project/branches/show' + end + module Commit autoload :Show, 'qa/page/project/commit/show' end @@ -191,6 +195,11 @@ module QA autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories' end + module SubMenus + autoload :Common, 'qa/page/project/sub_menus/common' + autoload :Repository, 'qa/page/project/sub_menus/repository' + end + module Issue autoload :New, 'qa/page/project/issue/new' autoload :Show, 'qa/page/project/issue/show' diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 7f959441dac..86e00cdbb9c 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -46,12 +46,9 @@ module QA run("git clone #{opts} #{uri} ./") end - def checkout(branch_name) - run(%Q{git checkout "#{branch_name}"}) - end - - def checkout_new_branch(branch_name) - run(%Q{git checkout -b "#{branch_name}"}) + def checkout(branch_name, new_branch: false) + opts = new_branch ? '-b' : '' + run(%Q{git checkout #{opts} "#{branch_name}"}).to_s end def shallow_clone @@ -84,6 +81,10 @@ module QA run("git push #{uri} #{branch}") end + def merge(branch) + run("git merge #{branch}") + end + def commits run('git log --oneline').split("\n") end diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb index 323acd57743..9344371a0b6 100644 --- a/qa/qa/page/label/index.rb +++ b/qa/qa/page/label/index.rb @@ -6,7 +6,22 @@ module QA element :label_create_new end + view 'app/views/shared/empty_states/_labels.html.haml' do + element :label_svg + end + + view 'app/assets/javascripts/lazy_loader.js' do + element :js_lazy_loaded + end + def go_to_new_label + # The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit + # This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?) + # before clicking the button. + within_element(:label_svg) do + has_element?(:js_lazy_loaded) + end + click_element :label_create_new end end diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb index 9191dbe9cf3..8c12eff5cf1 100644 --- a/qa/qa/page/profile/personal_access_tokens.rb +++ b/qa/qa/page/profile/personal_access_tokens.rb @@ -3,29 +3,51 @@ module QA module Profile class PersonalAccessTokens < Page::Base view 'app/views/shared/_personal_access_tokens_form.html.haml' do - element :personal_access_token_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern - element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck - element :scopes_api_radios, "label :scopes" # rubocop:disable QA/ElementWithPattern + element :personal_access_token_name_field + element :create_token_button + end + + view 'app/views/shared/tokens/_scopes_form.html.haml' do + element :api_radio, 'qa-#{scope}-radio' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck end view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do - element :create_token_field, "text_field_tag 'created-personal-access-token'" # rubocop:disable QA/ElementWithPattern + element :created_personal_access_token + end + view 'app/views/shared/_personal_access_tokens_table.html.haml' do + element :revoke_button end def fill_token_name(name) - fill_in 'personal_access_token_name', with: name + fill_element(:personal_access_token_name_field, name) end def check_api - check 'personal_access_token_scopes_api' + check_element(:api_radio) end def create_token - click_on 'Create personal access token' + click_element(:create_token_button) end def created_access_token - page.find('#created-personal-access-token').value + find_element(:created_personal_access_token, wait: 30).value + end + + def has_token_row_for_name?(token_name) + page.has_css?('tr', text: token_name, wait: 1.0) + end + + def first_token_row_for_name(token_name) + page.find('tr', text: token_name, match: :first, wait: 1.0) + end + + def revoke_first_token_with_name(token_name) + within first_token_row_for_name(token_name) do + accept_confirm do + click_element(:revoke_button) + end + end end end end diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb new file mode 100644 index 00000000000..762a97e2088 --- /dev/null +++ b/qa/qa/page/project/branches/show.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Branches + class Show < Page::Base + view 'app/views/projects/branches/_branch.html.haml' do + element :remove_btn + end + view 'app/views/projects/branches/_panel.html.haml' do + element :all_branches + end + view 'app/views/projects/branches/index.html.haml' do + element :delete_merged_branches + end + + def delete_branch(branch_name) + within_element(:all_branches) do + within(".js-branch-#{branch_name}") do + accept_alert do + find_element(:remove_btn).click + end + end + end + end + + def has_branch_title?(branch_title) + within_element(:all_branches) do + within(".item-title") do + has_text?(branch_title) + end + end + end + + def has_branch_with_badge?(branch_name, badge) + within_element(:all_branches) do + within(".js-branch-#{branch_name} .badge") do + has_text?(badge) + end + end + end + + def delete_merged_branches + accept_alert do + click_element(:delete_merged_branches) + end + end + + def wait_for_texts_not_to_be_visible(texts) + text_not_visible = wait do + texts.all? do |text| + has_no_text?(text) + end + end + raise "Expected text(s) #{texts} not to be visible" unless text_not_visible + end + end + end + end + end +end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 835e1ed00b5..eb3426b1ac0 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -4,11 +4,14 @@ module QA module Page module Project class Menu < Page::Base + include SubMenus::Common + include SubMenus::Repository + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item element :settings_link, 'link_to edit_project_path' # rubocop:disable QA/ElementWithPattern - element :repository_link, "title: _('Repository')" # rubocop:disable QA/ElementWithPattern element :link_pipelines + element :link_operations element :link_members_settings element :pipelines_settings_link, "title: _('CI / CD')" # rubocop:disable QA/ElementWithPattern element :operations_kubernetes_link, "title: _('Kubernetes')" # rubocop:disable QA/ElementWithPattern @@ -18,7 +21,6 @@ module QA element :merge_requests_link, /link_to.*shortcuts-merge_requests/ # rubocop:disable QA/ElementWithPattern element :merge_requests_link_text, "Merge Requests" # rubocop:disable QA/ElementWithPattern element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern - element :operations_section, "class: 'shortcuts-operations'" # rubocop:disable QA/ElementWithPattern element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern element :milestones_link @@ -85,12 +87,6 @@ module QA end end - def click_repository - within_sidebar do - click_link('Repository') - end - end - def click_repository_settings hover_settings do within_submenu do @@ -129,6 +125,7 @@ module QA def hover_issues within_sidebar do + scroll_to_element(:issues_item) find_element(:issues_item).hover yield @@ -137,7 +134,8 @@ module QA def hover_operations within_sidebar do - find('.shortcuts-operations').hover + scroll_to_element(:link_operations) + find_element(:link_operations).hover yield end @@ -145,20 +143,9 @@ module QA def hover_settings within_sidebar do - find('.qa-settings-item').hover - - yield - end - end - - def within_sidebar - page.within('.sidebar-top-level-items') do - yield - end - end + scroll_to_element(:settings_item) + find_element(:settings_item).hover - def within_submenu - page.within('.fly-out-list') do yield end end diff --git a/qa/qa/page/project/sub_menus/common.rb b/qa/qa/page/project/sub_menus/common.rb new file mode 100644 index 00000000000..c94e1e85256 --- /dev/null +++ b/qa/qa/page/project/sub_menus/common.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Common + def within_sidebar + within('.sidebar-top-level-items') do + yield + end + end + + def within_submenu + within('.fly-out-list') do + yield + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb new file mode 100644 index 00000000000..29eaa9a74de --- /dev/null +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Repository + def self.included(base) + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :project_menu_repo + element :branches_link + end + end + end + + def click_repository + within_sidebar do + click_element(:project_menu_repo) + end + end + + def click_repository_branches + hover_repository do + within_submenu do + click_element(:branches_link) + end + end + end + + private + + def hover_repository + within_sidebar do + find_element(:project_menu_repo).hover + + yield + end + end + end + end + end + end +end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index f325162d1c0..ffe8633dd16 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -116,23 +116,13 @@ module QA end private_class_method :evaluator - def self.dynamic_attributes - const_get(:DynamicAttributes) - rescue NameError - mod = const_set(:DynamicAttributes, Module.new) - - include mod - - mod - end - class DSL def initialize(base) @base = base end def attribute(name, &block) - @base.dynamic_attributes.module_eval do + @base.module_eval do attr_writer(name) define_method(name) do diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb index 9fd66f3a36a..c6243ff43fa 100644 --- a/qa/qa/resource/fork.rb +++ b/qa/qa/resource/fork.rb @@ -3,6 +3,13 @@ module QA module Resource class Fork < Base + attribute :project do + Resource::Project.fabricate! do |resource| + resource.name = push.project.name + resource.path_with_namespace = "#{user.name}/#{push.project.name}" + end + end + attribute :push do Repository::ProjectPush.fabricate! end @@ -37,6 +44,8 @@ module QA Page::Layout::Banner.perform do |page| page.has_notice?('The project was successfully forked.') end + + populate(:project) end end end diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb index f91ae299d76..5d20a6e9c75 100644 --- a/qa/qa/resource/merge_request_from_fork.rb +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -11,7 +11,7 @@ module QA attribute :push do Repository::ProjectPush.fabricate! do |resource| - resource.project = fork + resource.project = fork.project resource.branch_name = fork_branch resource.file_name = 'file2.txt' resource.user = fork.user diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 1fafbf5d73e..433e5a8f7c9 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -12,6 +12,10 @@ module QA Group.fabricate! end + attribute :path_with_namespace do + "#{group.sandbox.path}/#{group.path}/#{name}" if group + end + attribute :repository_ssh_location do Page::Project::Show.perform do |page| page.repository_clone_ssh_location @@ -46,8 +50,14 @@ module QA end end + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + def api_get_path - "/projects/#{name}" + "/projects/#{CGI.escape(path_with_namespace)}" end def api_post_path diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb index c14d97ff7fb..f33aa0acef0 100644 --- a/qa/qa/resource/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -64,7 +64,7 @@ module QA repository.configure_identity(username, email) if new_branch - repository.checkout_new_branch(branch_name) + repository.checkout(branch_name, new_branch: true) else repository.checkout(branch_name) end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 79b40223d84..23a2ace6a55 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -63,7 +63,7 @@ module QA # - "https://user:pass@somehost.com:443/wd/hub" # - "http://localhost:4444/wd/hub" - return unless ENV['QA_REMOTE_GRID'] + return if (ENV['QA_REMOTE_GRID'] || '').empty? "#{remote_grid_protocol}://#{remote_grid_credentials}#{ENV['QA_REMOTE_GRID']}/wd/hub" end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 6dcd74471fe..6ca7af8a3af 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -5,18 +5,18 @@ module QA describe 'Merge request creation from fork' do it 'user forks a project, submits a merge request and maintainer merges it' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| merge_request.fork_branch = 'feature-branch' end - Page::Main::Menu.perform { |main| main.sign_out } - Page::Main::Login.perform { |login| login.sign_in_using_credentials } + Page::Main::Menu.perform(&:sign_out) + Page::Main::Login.perform(&:sign_in_using_credentials) merge_request.visit! - Page::MergeRequest::Show.perform { |show| show.merge! } + Page::MergeRequest::Show.perform(&:merge!) expect(page).to have_content('The changes were merged') end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb new file mode 100644 index 00000000000..0f0c627d79a --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Create, list, and delete branches via web' do + master_branch = 'master' + second_branch = 'second-branch' + third_branch = 'third-branch' + file_1_master = 'file.txt' + file_2_master = 'other-file.txt' + file_second_branch = 'file-2.txt' + file_third_branch = 'file-3.txt' + first_commit_message_of_master_branch = "Add #{file_1_master}" + second_commit_message_of_master_branch = "Add #{file_2_master}" + commit_message_of_second_branch = "Add #{file_second_branch}" + commit_message_of_third_branch = "Add #{file_third_branch}" + + before do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + project = Resource::Project.fabricate! do |proj| + proj.name = 'project-qa-test' + proj.description = 'project for qa test' + end + project.visit! + + Git::Repository.perform do |repository| + repository.uri = project.repository_http_location.uri + repository.use_default_credentials + + repository.act do + clone + configure_identity('GitLab QA', 'root@gitlab.com') + commit_file(file_1_master, 'Test file content', first_commit_message_of_master_branch) + push_changes + checkout(second_branch, new_branch: true) + commit_file(file_second_branch, 'File 2 content', commit_message_of_second_branch) + push_changes(second_branch) + checkout(master_branch) + # This second commit on master is needed for the master branch to be ahead + # of the second branch, and when the second branch is merged to master it will + # show the 'merged' badge on it. + # Refer to the below issue note: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55524#note_126100848 + commit_file(file_2_master, 'Other test file content', second_commit_message_of_master_branch) + push_changes + merge(second_branch) + push_changes + checkout(third_branch, new_branch: true) + commit_file(file_third_branch, 'File 3 content', commit_message_of_third_branch) + push_changes(third_branch) + end + end + Page::Project::Show.perform(&:wait_for_push) + end + + it 'branches are correctly listed after CRUD operations' do + Page::Project::Menu.perform(&:click_repository_branches) + + expect(page).to have_content(master_branch) + expect(page).to have_content(second_branch) + expect(page).to have_content(third_branch) + expect(page).to have_content("Merge branch 'second-branch'") + expect(page).to have_content(commit_message_of_second_branch) + expect(page).to have_content(commit_message_of_third_branch) + + Page::Project::Branches::Show.perform do |branches| + expect(branches).to have_branch_with_badge(second_branch, 'merged') + end + + Page::Project::Branches::Show.perform do |branches_view| + branches_view.delete_branch(third_branch) + end + + expect(page).not_to have_content(third_branch) + + Page::Project::Branches::Show.perform(&:delete_merged_branches) + + expect(page).to have_content( + 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.' + ) + + page.refresh + Page::Project::Branches::Show.perform do |branches_view| + branches_view.wait_for_texts_not_to_be_visible([commit_message_of_second_branch]) + expect(branches_view).not_to have_branch_title(second_branch) + end + end + end + end +end diff --git a/qa/qa/tools/revoke_all_personal_access_tokens.rb b/qa/qa/tools/revoke_all_personal_access_tokens.rb new file mode 100644 index 00000000000..7484b633bf6 --- /dev/null +++ b/qa/qa/tools/revoke_all_personal_access_tokens.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative '../../qa' +require 'net/protocol.rb' +# This script revokes all personal access tokens with the name of 'api-test-token' on the host specified by GITLAB_ADDRESS +# Required environment variables: GITLAB_USERNAME, GITLAB_PASSWORD and GITLAB_ADDRESS +# Run `rake revoke_personal_access_tokens` + +module QA + module Tools + class RevokeAllPersonalAccessTokens + def run + do_run + rescue Net::ReadTimeout + STDOUT.puts 'Net::ReadTimeout during run. Trying again' + run + end + + private + + def do_run + raise ArgumentError, "Please provide GITLAB_USERNAME" unless ENV['GITLAB_USERNAME'] + raise ArgumentError, "Please provide GITLAB_PASSWORD" unless ENV['GITLAB_PASSWORD'] + raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] + + STDOUT.puts 'Running...' + + Runtime::Browser.visit(ENV['GITLAB_ADDRESS'], Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) + + token_name = 'api-test-token' + + Page::Profile::PersonalAccessTokens.perform do |page| + while page.has_token_row_for_name?(token_name) + page.revoke_first_token_with_name(token_name) + print "\e[32m.\e[0m" + end + end + end + end + end +end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index b8c406ae72a..a2a3ad01749 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -213,6 +213,42 @@ describe QA::Resource::Base do .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.") end end + + context 'when multiple resources have the same attribute name' do + let(:base) do + Class.new(QA::Resource::Base) do + def fabricate! + 'any' + end + + def self.current_url + 'http://stub' + end + end + end + let(:first_resource) do + Class.new(base) do + attribute :test do + 'first block' + end + end + end + let(:second_resource) do + Class.new(base) do + attribute :test do + 'second block' + end + end + end + + it 'has unique attribute values' do + first_result = first_resource.fabricate!(resource: first_resource.new) + second_result = second_resource.fabricate!(resource: second_resource.new) + + expect(first_result.test).to eq 'first block' + expect(second_result.test).to eq 'second block' + end + end end describe '#web_url' do diff --git a/rubocop/cop/inject_enterprise_edition_module.rb b/rubocop/cop/inject_enterprise_edition_module.rb index c8b8aca51ab..1d37b1bd12d 100644 --- a/rubocop/cop/inject_enterprise_edition_module.rb +++ b/rubocop/cop/inject_enterprise_edition_module.rb @@ -11,9 +11,13 @@ module RuboCop METHODS = Set.new(%i[include extend prepend]).freeze - def_node_matcher :ee_const?, <<~PATTERN - (const (const _ :EE) _) - PATTERN + def ee_const?(node) + line = node.location.expression.source_line + + # We use `match?` here instead of RuboCop's AST matching, as this makes + # it far easier to handle nested constants such as `EE::Foo::Bar::Baz`. + line.match?(/(\s|\()(::)?EE::/) + end def on_send(node) return unless METHODS.include?(node.children[1]) diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb index 9bf5f1e3b18..63c1b975a65 100644 --- a/rubocop/spec_helpers.rb +++ b/rubocop/spec_helpers.rb @@ -1,6 +1,7 @@ module RuboCop module SpecHelpers SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze + MIGRATION_SPEC_DIRECTORIES = ['spec/migrations', 'spec/lib/gitlab/background_migration'].freeze # Returns true if the given node originated from the spec directory. def in_spec?(node) @@ -10,14 +11,18 @@ module RuboCop path.start_with?(File.join(Dir.pwd, 'spec'), File.join(Dir.pwd, 'ee', 'spec')) end + def migration_directories + @migration_directories ||= MIGRATION_SPEC_DIRECTORIES.map do |dir| + [File.join(Dir.pwd, dir), File.join(Dir.pwd, 'ee', dir)] + end.flatten + end + # Returns true if the given node originated from a migration spec. def in_migration_spec?(node) path = node.location.expression.source_buffer.name in_spec?(node) && - path.start_with?( - File.join(Dir.pwd, 'spec', 'migrations'), - File.join(Dir.pwd, 'ee', 'spec', 'migrations')) + path.start_with?(*migration_directories) end end end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 4e1dbff7b80..1ee6f502b8e 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -346,7 +346,6 @@ function wait_for_job_to_be_done() { if [[ "${job_status}" == "failed" ]]; then echo "The '${job_name}' failed." - exit 1 elif [[ "${job_status}" == "manual" ]]; then echo "The '${job_name}' is manual." else diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 43f561f7a25..c290acb72aa 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -519,12 +519,14 @@ describe ApplicationController do get :index expect(response).to have_gitlab_http_status(404) + expect(response).to render_template('errors/not_found') end it 'renders a 403 when a message is passed to access denied' do get :index, params: { message: 'None shall pass' } expect(response).to have_gitlab_http_status(403) + expect(response).to render_template('errors/access_denied') end it 'renders a status passed to access denied' do diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index 73195463a50..bb282db5a41 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -78,7 +78,7 @@ describe Import::BitbucketServerController do end it "returns an error when the server can't be contacted" do - expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(BitbucketServer::Client::ServerError) + expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError) post :create, params: { project: project_key, repository: repo_slug }, format: :json diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 21936491ffc..59463462e5a 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -55,7 +55,7 @@ describe OmniauthCallbacksController, type: :controller do context 'when a redirect url is stored' do it 'redirects with fragment' do - post provider, nil, { user_return_to: '/fake/url' } + post provider, session: { user_return_to: '/fake/url' } expect(response).to redirect_to('/fake/url#L101') end @@ -63,7 +63,7 @@ describe OmniauthCallbacksController, type: :controller do context 'when a redirect url with a fragment is stored' do it 'redirects with the new fragment' do - post provider, nil, { user_return_to: '/fake/url#replaceme' } + post provider, session: { user_return_to: '/fake/url#replaceme' } expect(response).to redirect_to('/fake/url#L101') end diff --git a/spec/controllers/projects/badges_controller_spec.rb b/spec/controllers/projects/badges_controller_spec.rb index 2556bc3ae50..8eac3d9a459 100644 --- a/spec/controllers/projects/badges_controller_spec.rb +++ b/spec/controllers/projects/badges_controller_spec.rb @@ -22,7 +22,44 @@ describe Projects::BadgesController do expect(response).to have_gitlab_http_status(:ok) end - def get_badge(badge) - get badge, params: { namespace_id: project.namespace.to_param, project_id: project, ref: pipeline.ref }, format: :svg + it 'renders the `flat` badge layout by default' do + get_badge(:coverage) + + expect(response).to render_template('projects/badges/badge') + end + + context 'when style param is set to `flat`' do + it 'renders the `flat` badge layout' do + get_badge(:coverage, 'flat') + + expect(response).to render_template('projects/badges/badge') + end + end + + context 'when style param is set to an invalid type' do + it 'renders the `flat` (default) badge layout' do + get_badge(:coverage, 'xxx') + + expect(response).to render_template('projects/badges/badge') + end + end + + context 'when style param is set to `flat-square`' do + it 'renders the `flat-square` badge layout' do + get_badge(:coverage, 'flat-square') + + expect(response).to render_template('projects/badges/badge_flat-square') + end + end + + def get_badge(badge, style = nil) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + ref: pipeline.ref, + style: style + } + + get badge, params: params, format: :svg end end diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb index 729e71b87a6..6464398cea1 100644 --- a/spec/controllers/projects/error_tracking_controller_spec.rb +++ b/spec/controllers/projects/error_tracking_controller_spec.rb @@ -20,18 +20,6 @@ describe Projects::ErrorTrackingController do expect(response).to render_template(:index) end - context 'with feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'returns 404' do - get :index, params: project_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - context 'with insufficient permissions' do before do project.add_guest(user) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index df21dc7bc85..a2c3bb2919d 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1030,19 +1030,6 @@ describe Projects::IssuesController do let(:project) { create(:project, :public) } let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') } - context 'feature disabled' do - it 'returns 404' do - sign_in(user) - project.add_maintainer(user) - - stub_feature_flags(issues_import_csv: false) - - import_csv - - expect(response).to have_gitlab_http_status :not_found - end - end - context 'unauthorized' do it 'returns 404 for guests' do sign_out(:user) @@ -1086,9 +1073,9 @@ describe Projects::IssuesController do end def import_csv - post :import_csv, namespace_id: project.namespace.to_param, - project_id: project.to_param, - file: file + post :import_csv, params: { namespace_id: project.namespace.to_param, + project_id: project.to_param, + file: file } end end @@ -1131,6 +1118,7 @@ describe Projects::IssuesController do context 'when user is setting notes filters' do let(:issuable) { issue } + let(:issuable_parent) { project } let!(:discussion_note) { create(:discussion_note_on_issue, :system, noteable: issuable, project: project) } it_behaves_like 'issuable notes filter' diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 7f65fe551e9..d8a331b3cf0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -770,50 +770,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - describe 'POST cancel_all' do - before do - project.add_developer(user) - sign_in(user) - end - - context 'when jobs are cancelable' do - before do - create_list(:ci_build, 2, :cancelable, pipeline: pipeline) - - post_cancel_all - end - - it 'redirects to a index page' do - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(namespace_project_jobs_path) - end - - it 'transits to canceled' do - expect(Ci::Build.all).to all(be_canceled) - end - end - - context 'when jobs are not cancelable' do - before do - create_list(:ci_build, 2, :canceled, pipeline: pipeline) - - post_cancel_all - end - - it 'redirects to a index page' do - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(namespace_project_jobs_path) - end - end - - def post_cancel_all - post :cancel_all, params: { - namespace_id: project.namespace, - project_id: project - } - end - end - describe 'POST erase' do let(:role) { :maintainer } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 4f4d3ca226f..4451fd227e8 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -78,6 +78,7 @@ describe Projects::MergeRequestsController do context 'when user is setting notes filters' do let(:issuable) { merge_request } + let(:issuable_parent) { project } let!(:discussion_note) { create(:discussion_note_on_merge_request, :system, noteable: issuable, project: project) } let!(:discussion_comment) { create(:discussion_note_on_merge_request, noteable: issuable, project: project) } diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb index 382c1b5d124..4b742a5d427 100644 --- a/spec/controllers/projects/pages_controller_spec.rb +++ b/spec/controllers/projects/pages_controller_spec.rb @@ -28,10 +28,10 @@ describe Projects::PagesController do let(:group) { create(:group, :nested) } let(:project) { create(:project, namespace: group) } - it 'returns a 404 status code' do + it 'returns a 200 status code' do get :show, params: request_params - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(200) end end end diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb index 810f5bb64ba..d989ec22481 100644 --- a/spec/controllers/projects/settings/operations_controller_spec.rb +++ b/spec/controllers/projects/settings/operations_controller_spec.rb @@ -41,18 +41,6 @@ describe Projects::Settings::OperationsController do end end - context 'with feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'renders 404' do - get :show, params: project_params(project) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - context 'with insufficient permissions' do before do project.add_reporter(user) @@ -121,18 +109,6 @@ describe Projects::Settings::OperationsController do end end - context 'with feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'renders 404' do - patch :update, params: project_params(project) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - context 'with insufficient permissions' do before do project.add_reporter(user) diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index 2335b5118dd..ae257d769e8 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -5,7 +5,7 @@ FactoryBot.define do transient do attrs do { - title: 'Title', + title: 'Title.with.dot', content: 'Content for wiki page', format: 'markdown' } diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index baa2b1d8af5..08c27354bd2 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -97,7 +97,7 @@ describe 'Issue Boards', :js do expect(find('.board:nth-child(4)')).to have_selector('.board-card') end - it 'shows description tooltip on list title' do + it 'shows description tooltip on list title', :quarantine do page.within('.board:nth-child(2)') do expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') end @@ -411,7 +411,7 @@ describe 'Issue Boards', :js do wait_for_empty_boards((2..4)) end - it 'filters by label with space after reload' do + it 'filters by label with space after reload', :quarantine do set_filter("label", "\"#{accepting.title}") click_filter_link(accepting.title) submit_filter @@ -477,7 +477,7 @@ describe 'Issue Boards', :js do end end - it 'filters by multiple labels' do + it 'filters by multiple labels', :quarantine do set_filter("label", testing.title) click_filter_link(testing.title) diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb index 68bfbf22736..fa12cecc984 100644 --- a/spec/features/dashboard/help_spec.rb +++ b/spec/features/dashboard/help_spec.rb @@ -5,13 +5,23 @@ RSpec.describe 'Dashboard Help' do sign_in(create(:user)) end - it 'renders correctly markdown' do - visit help_page_path("administration/raketasks/maintenance") + context 'help dropdown' do + it 'shows the "What\'s new?" menu item' do + visit root_dashboard_path - expect(page).to have_content('Gather information about GitLab and the system it runs on') + expect(page.find('.header-help .dropdown-menu')).to have_text("What's new?") + end + end + + context 'documentation' do + it 'renders correctly markdown' do + visit help_page_path("administration/raketasks/maintenance") + + expect(page).to have_content('Gather information about GitLab and the system it runs on') - node = find('.documentation h2 a#user-content-check-gitlab-configuration') - expect(node[:href]).to eq '#check-gitlab-configuration' - expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + node = find('.documentation h2 a#user-content-check-gitlab-configuration') + expect(node[:href]).to eq '#check-gitlab-configuration' + expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + end end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 975b7944741..edca8f9df08 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -91,6 +91,7 @@ describe 'Dashboard Projects' do visit dashboard_projects_path expect(page).to have_content(project.name) + expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1) end it 'shows personal projects on personal projects tab', :js do @@ -121,6 +122,8 @@ describe 'Dashboard Projects' do expect(page).not_to have_content(project.name) expect(page).to have_content(project2.name) + expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1) + expect(find('.nav-links li:nth-child(2) .badge-pill')).to have_content(1) end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 96b22a0f64b..2284ee925a0 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -332,7 +332,7 @@ describe 'Dashboard Todos' do it 'links to the pipelines for the merge request' do href = pipelines_project_merge_request_path(project, todo.target) - expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href + expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href: href end end end diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb index 6a23d6b78ab..678ce80b382 100644 --- a/spec/features/markdown/math_spec.rb +++ b/spec/features/markdown/math_spec.rb @@ -16,7 +16,7 @@ describe 'Math rendering', :js do visit project_issue_path(project, issue) - expect(page).to have_selector('.katex .mord.mathit', text: 'b') - expect(page).to have_selector('.katex-display .mord.mathit', text: 'b') + expect(page).to have_selector('.katex .mord.mathdefault', text: 'b') + expect(page).to have_selector('.katex-display .mord.mathdefault', text: 'b') end end diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb index ea61f9675bc..84636ae355c 100644 --- a/spec/features/merge_request/user_merges_immediately_spec.rb +++ b/spec/features/merge_request/user_merges_immediately_spec.rb @@ -25,6 +25,8 @@ describe 'Merge requests > User merges immediately', :js do end it 'enables merge immediately' do + wait_for_requests + page.within '.mr-widget-body' do find('.dropdown-toggle').click diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index 0959f1b12f3..5188dc3625f 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -51,22 +51,52 @@ describe 'Merge request < User sees mini pipeline graph', :js do first('.mini-pipeline-graph-dropdown-toggle') end - it 'expands when hovered' do + # Status icon button styles should update as described in + # https://gitlab.com/gitlab-org/gitlab-ce/issues/42769 + it 'has unique styles for default, :hover, :active, and :focus states' do find('.mini-pipeline-graph-dropdown-toggle') - before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + default_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + default_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + default_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") toggle.hover find('.mini-pipeline-graph-dropdown-toggle') - after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + hover_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + hover_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + hover_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") - expect(before_width).to be < after_width - end + page.driver.browser.action.click_and_hold(toggle.native).perform - it 'shows dropdown caret when hovered' do - toggle.hover + find('.mini-pipeline-graph-dropdown-toggle') + active_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + active_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + active_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + + page.driver.browser.action.release(toggle.native) + .move_by(100, 100) + .perform + + find('.mini-pipeline-graph-dropdown-toggle') + focus_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + focus_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + focus_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + + expect(default_background_color).not_to eq(hover_background_color) + expect(hover_background_color).not_to eq(active_background_color) + expect(default_background_color).not_to eq(active_background_color) + + expect(default_foreground_color).not_to eq(hover_foreground_color) + expect(hover_foreground_color).not_to eq(active_foreground_color) + expect(default_foreground_color).not_to eq(active_foreground_color) + + expect(focus_background_color).to eq(hover_background_color) + expect(focus_foreground_color).to eq(hover_foreground_color) - expect(toggle).to have_selector('.fa-caret-down') + expect(default_box_shadow).to eq('none') + expect(hover_box_shadow).to eq('none') + expect(active_box_shadow).not_to eq('none') + expect(focus_box_shadow).not_to eq('none') end it 'shows tooltip when hovered' do diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb index 9ebbbaea911..5f630c9ffa4 100644 --- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb +++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb @@ -25,8 +25,8 @@ describe "User browses artifacts" do page.within(".tree-table") do expect(page).to have_no_content("..") .and have_content("other_artifacts_0.1.2") - .and have_content("ci_artifacts.txt") - .and have_content("rails_sample.jpg") + .and have_content("ci_artifacts.txt 27 Bytes") + .and have_content("rails_sample.jpg 34.4 KB") end page.within(".build-header") do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 60f37f4b74a..8230396a4cc 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -28,7 +28,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end it "shows Pending tab jobs" do - expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content job.short_sha expect(page).to have_content job.ref @@ -44,7 +43,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it "shows Running tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Running') - expect(page).to have_link 'Cancel running' expect(page).to have_content job.short_sha expect(page).to have_content job.ref expect(page).to have_content job.name @@ -60,7 +58,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it "shows Finished tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_content 'No jobs to show' - expect(page).to have_link 'Cancel running' end end @@ -75,7 +72,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).to have_content job.short_sha expect(page).to have_content job.ref expect(page).to have_content job.name - expect(page).not_to have_link 'Cancel running' end end @@ -94,23 +90,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end - describe "POST /:project/jobs/:id/cancel_all" do - before do - job.run! - visit project_jobs_path(project) - click_link "Cancel running" - end - - it 'shows all necessary content' do - expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content 'canceled' - expect(page).to have_content job.short_sha - expect(page).to have_content job.ref - expect(page).to have_content job.name - expect(page).not_to have_link 'Cancel running' - end - end - describe "GET /:project/jobs/:id" do context "Job from project" do let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline) } @@ -191,7 +170,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do href = new_project_issue_path(project, options) - page.within('.header-action-buttons') do + page.within('.build-sidebar') do expect(find('.js-new-issue')['href']).to include(href) end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 4706c28bb3d..3192c9ffad4 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -477,10 +477,11 @@ describe 'Pipeline', :js do end context 'when accessing failed jobs page' do - it 'fails to access the page' do - subject + it 'renders a 404 page' do + requests = inspect_requests { subject } - expect(page).to have_title('Access Denied') + expect(page).to have_title('Not Found') + expect(requests.first.status_code).to eq(404) end end end diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb index 1f2328a6dd8..06290c67c70 100644 --- a/spec/features/projects/settings/operations_settings_spec.rb +++ b/spec/features/projects/settings/operations_settings_spec.rb @@ -8,32 +8,16 @@ describe 'Projects > Settings > For a forked project', :js do let(:role) { :maintainer } before do - stub_feature_flags(error_tracking: true) sign_in(user) project.add_role(user, role) end describe 'Sidebar > Operations' do - context 'when sidebar feature flag enabled' do - it 'renders the settings link in the sidebar' do - visit project_path(project) - wait_for_requests + it 'renders the settings link in the sidebar' do + visit project_path(project) + wait_for_requests - expect(page).to have_selector('a[title="Operations"]', visible: false) - end - end - - context 'when sidebar feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'does not render the settings link in the sidebar' do - visit project_path(project) - wait_for_requests - - expect(page).not_to have_selector('a[title="Operations"]', visible: false) - end + expect(page).to have_selector('a[title="Operations"]', visible: false) end end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index b56bb272b46..eb70a3c41c1 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -99,6 +99,30 @@ describe 'Project' do end end + describe 'project topics' do + let(:project) { create(:project, :repository) } + let(:path) { project_path(project) } + + before do + sign_in(create(:admin)) + visit path + end + + it 'shows project topics' do + project.update_attribute(:tag_list, 'topic1') + visit path + expect(page).to have_css('.project-topic-list') + expect(page).to have_content('topic1') + end + + it 'shows up to 3 project tags' do + project.update_attribute(:tag_list, 'topic1, topic2, topic3, topic4') + visit path + expect(page).to have_css('.project-topic-list') + expect(page).to have_content('topic1, topic2, topic3 + 1 more') + end + end + describe 'copy clone URL to clipboard', :js do let(:project) { create(:project, :repository) } let(:path) { project_path(project) } diff --git a/spec/fixtures/malicious.bundle b/spec/fixtures/malicious.bundle new file mode 100644 index 00000000000..7ba47932906 --- /dev/null +++ b/spec/fixtures/malicious.bundle @@ -0,0 +1 @@ +gitdir: foo.git diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js index 7237274eb43..53b9ac22fc0 100644 --- a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js @@ -1 +1,34 @@ // TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue'; +import diffsMockData from '../mock_data/merge_request_diffs'; + +describe('CompareVersionsDropdown', () => { + let wrapper; + const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(CompareVersionsDropdown, { localVue, ...options }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render a correct base version link', () => { + factory({ + propsData: { + baseVersionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + otherVersions: diffsMockData.slice(1), + targetBranch, + }, + }); + + const links = wrapper.findAll('a'); + const lastLink = links.wrappers[links.length - 1]; + + expect(lastLink.attributes('href')).toEqual(wrapper.props('baseVersionPath')); + }); +}); diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 75c66e9ca82..50135b0cf86 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -100,6 +100,12 @@ describe('CompareVersions', () => { }); }); + describe('baseVersionPath', () => { + it('should be set correctly from mergeRequestDiff', () => { + expect(vm.baseVersionPath).toEqual(vm.mergeRequestDiff.base_version_path); + }); + }); + describe('isWhitespaceVisible', () => { const originalHref = window.location.href; diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js index d72ad7818dd..4bbef146336 100644 --- a/spec/javascripts/diffs/mock_data/merge_request_diffs.js +++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js @@ -1,42 +1,46 @@ export default [ { - versionIndex: 4, - createdAt: '2018-10-23T11:49:16.611Z', - commitsCount: 4, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + version_index: 4, + created_at: '2018-10-23T11:49:16.611Z', + commits_count: 4, latest: true, - shortCommitSha: 'de7a8f7f', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', - comparePath: + short_commit_sha: 'de7a8f7f', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995', }, { - versionIndex: 3, - createdAt: '2018-10-23T11:46:40.617Z', - commitsCount: 3, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + version_index: 3, + created_at: '2018-10-23T11:46:40.617Z', + commits_count: 3, latest: false, - shortCommitSha: 'e78fc18f', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', - comparePath: + short_commit_sha: 'e78fc18f', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e', }, { - versionIndex: 2, - createdAt: '2018-10-04T09:57:39.648Z', - commitsCount: 2, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + version_index: 2, + created_at: '2018-10-04T09:57:39.648Z', + commits_count: 2, latest: false, - shortCommitSha: '48da7e7e', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', - comparePath: + short_commit_sha: '48da7e7e', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e', }, { - versionIndex: 1, - createdAt: '2018-09-25T20:30:39.493Z', - commitsCount: 1, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + version_index: 1, + created_at: '2018-09-25T20:30:39.493Z', + commits_count: 1, latest: false, - shortCommitSha: '47bac2ed', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', - comparePath: + short_commit_sha: '47bac2ed', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0', }, ]; diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index d8733941181..c595c38ef55 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -628,4 +628,26 @@ describe('DiffsStoreMutations', () => { expect(file.parallel_diff_lines[1].right.hasForm).toBe(false); }); }); + + describe('SET_TREE_DATA', () => { + it('sets treeEntries and tree in state', () => { + const state = { + treeEntries: {}, + tree: [], + }; + + mutations[types.SET_TREE_DATA](state, { + treeEntries: { file: { name: 'index.js' } }, + tree: ['tree'], + }); + + expect(state.treeEntries).toEqual({ + file: { + name: 'index.js', + }, + }); + + expect(state.tree).toEqual(['tree']); + }); + }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index 036b320b314..ea86844ddca 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -601,4 +601,123 @@ describe('DiffsStoreUtils', () => { expect(utils.getDiffMode({})).toBe('replaced'); }); }); + + describe('getLowestSingleFolder', () => { + it('returns path and tree of lowest single folder tree', () => { + const folder = { + name: 'app', + type: 'tree', + tree: [ + { + name: 'javascripts', + type: 'tree', + tree: [ + { + type: 'blob', + name: 'index.js', + }, + ], + }, + ], + }; + const { path, treeAcc } = utils.getLowestSingleFolder(folder); + + expect(path).toEqual('app/javascripts'); + expect(treeAcc).toEqual([ + { + type: 'blob', + name: 'index.js', + }, + ]); + }); + + it('returns passed in folders path & tree when more than tree exists', () => { + const folder = { + name: 'app', + type: 'tree', + tree: [ + { + name: 'spec', + type: 'blob', + tree: [], + }, + ], + }; + const { path, treeAcc } = utils.getLowestSingleFolder(folder); + + expect(path).toEqual('app'); + expect(treeAcc).toBeNull(); + }); + }); + + describe('flattenTree', () => { + it('returns flattened directory structure', () => { + const tree = [ + { + type: 'tree', + name: 'app', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [ + { + type: 'blob', + name: 'index.js', + tree: [], + }, + ], + }, + ], + }, + { + type: 'tree', + name: 'spec', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [], + }, + { + type: 'blob', + name: 'index_spec.js', + tree: [], + }, + ], + }, + ]; + const flattened = utils.flattenTree(tree); + + expect(flattened).toEqual([ + { + type: 'tree', + name: 'app/javascripts', + tree: [ + { + type: 'blob', + name: 'index.js', + tree: [], + }, + ], + }, + { + type: 'tree', + name: 'spec', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [], + }, + { + type: 'blob', + name: 'index_spec.js', + tree: [], + }, + ], + }, + ]); + }); + }); }); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index b0bc16d7c64..3a02351460c 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -28,7 +28,7 @@ describe('Sidebar details block', () => { store, }); - expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); + expect(vm.$el.querySelector('.js-retry-button')).toBeNull(); }); }); @@ -70,7 +70,7 @@ describe('Sidebar details block', () => { }); it('should render link to retry job', () => { - expect(vm.$el.querySelector('.js-retry-job').getAttribute('href')).toEqual(job.retry_path); + expect(vm.$el.querySelector('.js-retry-button').getAttribute('href')).toEqual(job.retry_path); }); it('should render link to cancel job', () => { diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 4195d9d3680..7931b2af79f 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -8,30 +8,6 @@ describe('Job Store Getters', () => { localState = state(); }); - describe('headerActions', () => { - describe('with new issue path', () => { - it('returns an array with action to create a new issue', () => { - localState.job.new_issue_path = 'issues/new'; - - expect(getters.headerActions(localState)).toEqual([ - { - label: 'New issue', - path: localState.job.new_issue_path, - cssClass: - 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', - type: 'link', - }, - ]); - }); - }); - - describe('without new issue path', () => { - it('returns an empty array', () => { - expect(getters.headerActions(localState)).toEqual([]); - }); - }); - }); - describe('headerTime', () => { describe('when the job has started key', () => { it('returns started key', () => { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index f320f232687..0dc7e93539a 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -347,20 +347,31 @@ describe('common_utils', () => { }); describe('parseBoolean', () => { + const { parseBoolean } = commonUtils; + it('returns true for "true"', () => { - expect(commonUtils.parseBoolean('true')).toEqual(true); + expect(parseBoolean('true')).toEqual(true); }); it('returns false for "false"', () => { - expect(commonUtils.parseBoolean('false')).toEqual(false); + expect(parseBoolean('false')).toEqual(false); }); it('returns false for "something"', () => { - expect(commonUtils.parseBoolean('something')).toEqual(false); + expect(parseBoolean('something')).toEqual(false); }); it('returns false for null', () => { - expect(commonUtils.parseBoolean(null)).toEqual(false); + expect(parseBoolean(null)).toEqual(false); + }); + + it('is idempotent', () => { + const input = ['true', 'false', 'something', null]; + input.forEach(value => { + const result = parseBoolean(value); + + expect(parseBoolean(result)).toBe(result); + }); }); }); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 96c0844f83c..547379dabed 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -187,6 +187,7 @@ if (process.env.BABEL_ENV === 'coverage') { './terminal/terminal_bundle.js', './users/users_bundle.js', './issue_show/index.js', + './pages/admin/application_settings/show/index.js', ]; describe('Uncovered files', function() { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 2119a3b927a..e387367d1a2 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,11 +1,12 @@ import Vue from 'vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; +import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; const commitMessage = 'This is the commit message'; const commitMessageWithDescription = 'This is the commit message description'; -const createComponent = (customConfig = {}) => { - const Component = Vue.extend(ReadyToMerge); +const createTestMr = customConfig => { const mr = { isPipelineActive: false, pipeline: null, @@ -16,6 +17,7 @@ const createComponent = (customConfig = {}) => { hasCI: false, ciStatus: null, sha: '12345678', + squash: false, commitMessage, commitMessageWithDescription, shouldRemoveSourceBranch: true, @@ -24,14 +26,23 @@ const createComponent = (customConfig = {}) => { Object.assign(mr, customConfig.mr); - const service = { - merge() {}, - poll() {}, - }; + return mr; +}; + +const createTestService = () => ({ + merge() {}, + poll() {}, +}); + +const createComponent = (customConfig = {}) => { + const Component = Vue.extend(ReadyToMerge); return new Component({ el: document.createElement('div'), - propsData: { mr, service }, + propsData: { + mr: createTestMr(customConfig), + service: createTestService(), + }, }); }; @@ -612,6 +623,47 @@ describe('ReadyToMerge', () => { }); }); + describe('Squash checkbox component', () => { + let wrapper; + const localVue = createLocalVue(); + + const createLocalComponent = (customConfig = {}) => { + wrapper = shallowMount(localVue.extend(ReadyToMerge), { + localVue, + propsData: { + mr: createTestMr(customConfig), + service: createTestService(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); + + it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { + createLocalComponent({ + mr: { commitsCount: 2, enableSquashBeforeMerge: true }, + }); + + expect(findCheckboxElement().exists()).toBeTruthy(); + }); + + it('should not be rendered when squash before merge is disabled', () => { + createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); + + it('should not be rendered when there is only 1 commit', () => { + createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); + }); + describe('Merge controls', () => { describe('when allowed to merge', () => { beforeEach(() => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js new file mode 100644 index 00000000000..d6d8eecfcb9 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js @@ -0,0 +1,100 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; + +const localVue = createLocalVue(); + +describe('Squash before merge component', () => { + let wrapper; + + const createComponent = props => { + wrapper = shallowMount(localVue.extend(SquashBeforeMerge), { + localVue, + sync: false, + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('checkbox', () => { + const findCheckbox = () => wrapper.find('.qa-squash-checkbox'); + + it('is unchecked if passed value prop is false', () => { + createComponent({ + value: false, + }); + + expect(findCheckbox().element.checked).toBeFalsy(); + }); + + it('is checked if passed value prop is true', () => { + createComponent({ + value: true, + }); + + expect(findCheckbox().element.checked).toBeTruthy(); + }); + + it('changes value on click', done => { + createComponent({ + value: false, + }); + + findCheckbox().element.checked = true; + + findCheckbox().trigger('change'); + + wrapper.vm.$nextTick(() => { + expect(findCheckbox().element.checked).toBeTruthy(); + done(); + }); + }); + + it('is disabled if isDisabled prop is true', () => { + createComponent({ + value: false, + isDisabled: true, + }); + + expect(findCheckbox().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('about link', () => { + it('is not rendered if no help path is passed', () => { + createComponent({ + value: false, + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.exists()).toBeFalsy(); + }); + + it('is rendered if help path is passed', () => { + createComponent({ + value: false, + helpPath: 'test-path', + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.exists()).toBeTruthy(); + }); + + it('should have a correct help path if passed', () => { + createComponent({ + value: false, + helpPath: 'test-path', + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.attributes('href')).toEqual('test-path'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js index 8187b3204b1..12ee804f668 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -31,6 +31,12 @@ describe('Suggestion Diff component', () => { expect(header.innerHTML.includes('Suggested change')).toBe(true); }); + it('renders a help button', () => { + const helpBtn = vm.$el.querySelector('.js-help-btn'); + + expect(helpBtn).not.toBeNull(); + }); + it('renders an apply button', () => { const applyBtn = vm.$el.querySelector('.qa-apply-btn'); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js index 423cd6dee0f..33be63a3a1e 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js @@ -61,7 +61,7 @@ describe('Suggestion component', () => { describe('mounted', () => { it('renders a flash container', () => { - expect(vm.$el.querySelector('.flash-container')).not.toBeNull(); + expect(vm.$el.querySelector('.js-suggestions-flash')).not.toBeNull(); }); it('renders a container for suggestions', () => { diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js index 64aa7e29718..96bc3b0cc17 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -6,6 +6,8 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link const TEST_IMAGE_SIZE = 7; const TEST_BREAKPOINT = 5; +const TEST_EMPTY_MESSAGE = 'Lorem ipsum empty'; +const DEFAULT_EMPTY_MESSAGE = 'None'; const createUser = id => ({ id, @@ -21,14 +23,19 @@ const createList = n => const localVue = createLocalVue(); describe('UserAvatarList', () => { - let propsData; + let props; let wrapper; - const factory = options => { + const factory = (options = {}) => { + const propsData = { + ...props, + ...options.propsData, + }; + wrapper = shallowMount(localVue.extend(UserAvatarList), { + ...options, localVue, propsData, - ...options, }); }; @@ -38,28 +45,47 @@ describe('UserAvatarList', () => { }; beforeEach(() => { - propsData = { imgSize: TEST_IMAGE_SIZE }; + props = { imgSize: TEST_IMAGE_SIZE }; }); afterEach(() => { wrapper.destroy(); }); + describe('empty text', () => { + it('shows when items are empty', () => { + factory({ propsData: { items: [] } }); + + expect(wrapper.text()).toContain(DEFAULT_EMPTY_MESSAGE); + }); + + it('does not show when items are not empty', () => { + factory({ propsData: { items: createList(1) } }); + + expect(wrapper.text()).not.toContain(DEFAULT_EMPTY_MESSAGE); + }); + + it('can be set in props', () => { + factory({ propsData: { items: [], emptyText: TEST_EMPTY_MESSAGE } }); + + expect(wrapper.text()).toContain(TEST_EMPTY_MESSAGE); + }); + }); + describe('with no breakpoint', () => { beforeEach(() => { - propsData.breakpoint = 0; + props.breakpoint = 0; }); it('renders avatars', () => { const items = createList(20); - propsData.items = items; - factory(); + factory({ propsData: { items } }); const links = wrapper.findAll(UserAvatarLink); const linkProps = links.wrappers.map(x => x.props()); expect(linkProps).toEqual( - propsData.items.map(x => + items.map(x => jasmine.objectContaining({ linkHref: x.web_url, imgSrc: x.avatar_url, @@ -74,8 +100,8 @@ describe('UserAvatarList', () => { describe('with breakpoint and length equal to breakpoint', () => { beforeEach(() => { - propsData.breakpoint = TEST_BREAKPOINT; - propsData.items = createList(TEST_BREAKPOINT); + props.breakpoint = TEST_BREAKPOINT; + props.items = createList(TEST_BREAKPOINT); }); it('renders all avatars if length is <= breakpoint', () => { @@ -83,7 +109,7 @@ describe('UserAvatarList', () => { const links = wrapper.findAll(UserAvatarLink); - expect(links.length).toEqual(propsData.items.length); + expect(links.length).toEqual(props.items.length); }); it('does not show button', () => { @@ -95,8 +121,8 @@ describe('UserAvatarList', () => { describe('with breakpoint and length greater than breakpoint', () => { beforeEach(() => { - propsData.breakpoint = TEST_BREAKPOINT; - propsData.items = createList(TEST_BREAKPOINT + 1); + props.breakpoint = TEST_BREAKPOINT; + props.items = createList(TEST_BREAKPOINT + 1); }); it('renders avatars up to breakpoint', () => { @@ -116,7 +142,7 @@ describe('UserAvatarList', () => { it('renders all avatars', () => { const links = wrapper.findAll(UserAvatarLink); - expect(links.length).toEqual(propsData.items.length); + expect(links.length).toEqual(props.items.length); }); it('with collapse clicked, it renders avatars up to breakpoint', () => { diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb new file mode 100644 index 00000000000..2e50e4e2351 --- /dev/null +++ b/spec/lib/banzai/filter/footnote_filter_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::FootnoteFilter do + include FilterSpecHelper + + # first[^1] and second[^second] + # [^1]: one + # [^second]: two + let(:footnote) do + <<~EOF + <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p> + <ol> + <li id="fn1"> + <p>one <a href="#fnref1">↩</a></p> + </li> + <li id="fn2"> + <p>two <a href="#fnref2">↩</a></p> + </li> + </ol> + EOF + end + + let(:filtered_footnote) do + <<~EOF + <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p> + <section class="footnotes"><ol> + <li id="fn1-#{identifier}"> + <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p> + </li> + <li id="fn2-#{identifier}"> + <p>two <a href="#fnref2-#{identifier}" class="footnote-backref">↩</a></p> + </li> + </ol></section> + EOF + end + + context 'when footnotes exist' do + let(:doc) { filter(footnote) } + let(:link_node) { doc.css('sup > a').first } + let(:identifier) { link_node[:id].delete_prefix('fnref1-') } + + it 'properly adds the necessary ids and classes' do + expect(doc.to_html).to eq filtered_footnote + end + end +end diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 0b3c2390304..836af18e0b6 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -246,7 +246,7 @@ describe Banzai::Filter::SanitizationFilter do 'protocol-based JS injection: spaces and entities' => { input: '<a href="  javascript:alert(\'XSS\');">foo</a>', - output: '<a href>foo</a>' + output: '<a href="">foo</a>' }, 'protocol whitespace' => { @@ -300,5 +300,48 @@ describe Banzai::Filter::SanitizationFilter do expect(act.to_html).to eq exp end + + describe 'footnotes' do + it 'allows correct footnote id property on links' do + exp = %q{<a href="#fn1" id="fnref1">foo/bar.md</a>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'allows correct footnote id property on li element' do + exp = %q{<ol><li id="fn1">footnote</li></ol>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'removes invalid id for footnote links' do + exp = %q{<a href="#fn1">link</a>} + + %w[fnrefx test xfnref1].each do |id| + act = filter(%Q{<a href="#fn1" id="#{id}">link</a>}) + + expect(act.to_html).to eq exp + end + end + + it 'removes invalid id for footnote li' do + exp = %q{<ol><li>footnote</li></ol>} + + %w[fnx test xfn1].each do |id| + act = filter(%Q{<ol><li id="#{id}">footnote</li></ol>}) + + expect(act.to_html).to eq exp + end + end + + it 'allows footnotes numbered higher than 9' do + exp = %q{<a href="#fn15" id="fnref15">link</a><ol><li id="fn15">footnote</li></ol>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + end end end diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb index e9c7a2f352e..3634655c6a5 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -25,4 +25,36 @@ describe Banzai::Pipeline::FullPipeline do expect(result).to include(%{data-original='\">bad things'}) end end + + describe 'footnotes' do + let(:project) { create(:project, :public) } + let(:html) { described_class.to_html(footnote_markdown, project: project) } + let(:identifier) { html[/.*fnref1-(\d+).*/, 1] } + let(:footnote_markdown) do + <<~EOF + first[^1] and second[^second] + [^1]: one + [^second]: two + EOF + end + + let(:filtered_footnote) do + <<~EOF + <p dir="auto">first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p> + + <section class="footnotes"><ol> + <li id="fn1-#{identifier}"> + <p>one <a href="#fnref1-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p> + </li> + <li id="fn2-#{identifier}"> + <p>two <a href="#fnref2-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p> + </li> + </ol></section> + EOF + end + + it 'properly adds the necessary ids and classes' do + expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote + end + end end diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb index 5de0a9a65b5..b021e69800a 100644 --- a/spec/lib/bitbucket_server/client_spec.rb +++ b/spec/lib/bitbucket_server/client_spec.rb @@ -17,12 +17,6 @@ describe BitbucketServer::Client do subject.pull_requests(project, repo_slug) end - - it 'throws an exception when connection fails' do - allow(BitbucketServer::Collection).to receive(:new).and_raise(OpenSSL::SSL::SSLError) - - expect { subject.pull_requests(project, repo_slug) }.to raise_error(described_class::ServerError) - end end describe '#activities' do diff --git a/spec/lib/bitbucket_server/connection_spec.rb b/spec/lib/bitbucket_server/connection_spec.rb index b5da4cb1a49..ab8a5b89608 100644 --- a/spec/lib/bitbucket_server/connection_spec.rb +++ b/spec/lib/bitbucket_server/connection_spec.rb @@ -26,6 +26,12 @@ describe BitbucketServer::Connection do expect { subject.get(url) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.get(url) }.to raise_error(described_class::ConnectionError) + end end describe '#post' do @@ -42,6 +48,12 @@ describe BitbucketServer::Connection do expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) + end end describe '#delete' do @@ -63,6 +75,12 @@ describe BitbucketServer::Connection do expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:delete, branch_url).with(headers: headers).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) + end end end end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 9d56c62ae57..630732614b2 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -182,4 +182,18 @@ describe Feature do expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey end end + + describe Feature::Target do + describe '#targets' do + let(:project) { create(:project) } + let(:user_name) { project.owner.username } + + subject { described_class.new(user: user_name, project: project.full_path) } + + it 'returns all found targets' do + expect(subject.targets).to be_an(Array) + expect(subject.targets).to eq([project.owner, project]) + end + end + end end diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb new file mode 100644 index 00000000000..7f2979e8e28 --- /dev/null +++ b/spec/lib/gitlab/access/branch_protection_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Access::BranchProtection do + describe '#any?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | true + end + + with_them do + it { expect(described_class.new(level).any?).to eq(result) } + end + end + + describe '#developer_can_push?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false + Gitlab::Access::PROTECTION_FULL | false + end + + with_them do + it do + expect(described_class.new(level).developer_can_push?).to eq(result) + end + end + end + + describe '#developer_can_merge?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | false + end + + with_them do + it do + expect(described_class.new(level).developer_can_merge?).to eq(result) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb index 53c071f0268..510a0074554 100644 --- a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::BackfillProjectRepositories do let(:group) { create(:group, name: 'foo', path: 'foo') } @@ -34,6 +35,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do let!(:project_hashed_storage_2) { create(:project, name: 'bar', path: 'bar', namespace: group, storage_version: 2) } let!(:project_legacy_storage_3) { create(:project, name: 'baz', path: 'baz', namespace: group, storage_version: 0) } let!(:project_legacy_storage_4) { create(:project, name: 'zoo', path: 'zoo', namespace: group, storage_version: nil) } + let!(:project_legacy_storage_5) { create(:project, name: 'test', path: 'test', namespace: group, storage_version: nil) } describe '.on_hashed_storage' do it 'finds projects with repository on hashed storage' do @@ -47,7 +49,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do it 'finds projects with repository on legacy storage' do projects = described_class.on_legacy_storage.pluck(:id) - expect(projects).to match_array([project_legacy_storage_3.id, project_legacy_storage_4.id]) + expect(projects).to match_array([project_legacy_storage_3.id, project_legacy_storage_4.id, project_legacy_storage_5.id]) end end @@ -58,7 +60,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do projects = described_class.without_project_repository.pluck(:id) - expect(projects).to contain_exactly(project_hashed_storage_2.id, project_legacy_storage_4.id) + expect(projects).to contain_exactly(project_hashed_storage_2.id, project_legacy_storage_4.id, project_legacy_storage_5.id) end end @@ -78,17 +80,27 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do expect(project.disk_path).to eq(project_legacy_storage_3.disk_path) end + it 'returns the correct disk_path using the route entry' do + project_legacy_storage_5.route.update(path: 'zoo/new-test') + project = described_class.find(project_legacy_storage_5.id) + + expect(project.disk_path).to eq('zoo/new-test') + end + it 'raises OrphanedNamespaceError when any parent namespace does not exist' do subgroup = create(:group, parent: group) project_orphaned_namespace = create(:project, name: 'baz', path: 'baz', namespace: subgroup, storage_version: nil) subgroup.update_column(:parent_id, Namespace.maximum(:id).succ) project = described_class.find(project_orphaned_namespace.id) + project.route.destroy + subgroup.route.destroy - expect { project.disk_path } + expect { project.reload.disk_path } .to raise_error(Gitlab::BackgroundMigration::BackfillProjectRepositories::OrphanedNamespaceError) end end end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb index f92acf61682..f974dc8fda2 100644 --- a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb +++ b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration, schema: 20171005130944 do context 'when GpgKey exists' do - let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } + let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do GpgKeySubkey.destroy_all # rubocop: disable DestroyAll diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb index 1969aed51da..27281333348 100644 --- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, schema: 20180619121030 do describe '#perform' do context 'when diff files can be deleted' do @@ -71,3 +72,4 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs 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 5dce3fcbcb6..bc71a90605a 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,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :migration, schema: 20171114162227 do include GitHelpers @@ -324,3 +325,4 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index 5432d270555..188969951a6 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event, :migration, schema: 20170608152748 do describe '#commit_title' do it 'returns nil when there are no commits' do @@ -429,3 +430,4 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb index 021e1d14b18..ea8bdd48e72 100644 --- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do let(:migration) { described_class.new } @@ -17,3 +18,4 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete d end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb index 2e77e80ee46..593486fc56c 100644 --- a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb +++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'move_snippet_files_test') } let(:old_uploads_dir) { File.join('uploads', 'system', 'personal_snippet') } @@ -70,3 +71,4 @@ describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do "[an upload](#{path})" end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb index 4f1b01eed41..8e3cb36d313 100644 --- a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :migration, schema: 20181022173835 do let(:migration) { described_class.new } let(:clusters) { create_list(:cluster, 10, :project, :provided_by_gcp) } @@ -95,3 +96,4 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb index 6ab126ad39a..3e009fed0f1 100644 --- a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration, schema: 20180916011959 do let(:migration) { described_class.new } @@ -65,3 +66,4 @@ describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration it_behaves_like 'no changes' end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb index e99257e3481..ff1bd9f7850 100644 --- a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb @@ -1,5 +1,6 @@ require 'rails_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, :migration, schema: 20171128214150 do # commits_count attribute is added in a next migration before do @@ -128,3 +129,4 @@ describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 941ef33c8a4..7651f594a4c 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -160,8 +160,7 @@ describe Gitlab::Ci::Config::Entry::Global do variables: { 'VAR' => 'value' }, ignore: false, after_script: ['make clean'], - only: { refs: %w[branches tags] }, - except: {} }, + only: { refs: %w[branches tags] } }, spinach: { name: :spinach, before_script: [], script: %w[spinach], @@ -172,8 +171,7 @@ describe Gitlab::Ci::Config::Entry::Global do variables: {}, ignore: false, after_script: ['make clean'], - only: { refs: %w[branches tags] }, - except: {} } + only: { refs: %w[branches tags] } } ) end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 3d0b98eb238..0560eb42e4d 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -258,8 +258,7 @@ describe Gitlab::Ci::Config::Entry::Job do stage: 'test', ignore: false, after_script: %w[cleanup], - only: { refs: %w[branches tags] }, - except: {}) + only: { refs: %w[branches tags] }) end end end diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index d97be76f0e0..271ee30df3c 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -67,14 +67,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do script: %w[rspec], ignore: false, stage: 'test', - only: { refs: %w[branches tags] }, - except: {} }, + only: { refs: %w[branches tags] } }, spinach: { name: :spinach, script: %w[spinach], ignore: false, stage: 'test', - only: { refs: %w[branches tags] }, - except: {} }) + only: { refs: %w[branches tags] } }) end end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 83001b7fdd8..1c987e13a9a 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -168,8 +168,33 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + describe '#value' do + context 'when default value has been provided' do + before do + entry.default = { refs: %w[branches tags] } + end + + context 'when user overrides default values' do + let(:config) { { refs: %w[feature], variables: %w[$VARIABLE] } } + + it 'does not include default values' do + expect(entry.value).to eq config + end + end + + context 'when default value has not been defined' do + let(:config) { { variables: %w[$VARIABLE] } } + + it 'includes default values' do + expect(entry.value).to eq(refs: %w[branches tags], + variables: %w[$VARIABLE]) + end + end + end + end + describe '.default' do - it 'does not have a default value' do + it 'does not have default policy' do expect(described_class.default).to be_nil end end diff --git a/spec/lib/gitlab/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb index 85a7cf1d241..37e38e49c0d 100644 --- a/spec/lib/gitlab/config/entry/configurable_spec.rb +++ b/spec/lib/gitlab/config/entry/configurable_spec.rb @@ -7,6 +7,10 @@ describe Gitlab::Config::Entry::Configurable do end end + before do + allow(entry).to receive(:default) + end + describe 'validations' do context 'when entry is a hash' do let(:instance) { entry.new(key: 'value') } @@ -26,9 +30,11 @@ describe Gitlab::Config::Entry::Configurable do end describe 'configured entries' do + let(:entry_class) { double('entry_class', default: nil) } + before do - entry.class_eval do - entry :object, Object, description: 'test object' + entry.class_exec(entry_class) do |entry_class| + entry :object, entry_class, description: 'test object' end end diff --git a/spec/lib/gitlab/git/bundle_file_spec.rb b/spec/lib/gitlab/git/bundle_file_spec.rb new file mode 100644 index 00000000000..ff7c981dadd --- /dev/null +++ b/spec/lib/gitlab/git/bundle_file_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::Git::BundleFile do + describe '.check!' do + let(:valid_bundle) { Tempfile.new } + let(:valid_bundle_path) { valid_bundle.path } + let(:invalid_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') } + + after do + valid_bundle.close! + end + + it 'returns nil for a valid bundle' do + valid_bundle.write("# v2 git bundle\nfoo bar baz\n") + valid_bundle.close + + expect(described_class.check!(valid_bundle_path)).to be_nil + end + + it 'raises an exception for an invalid bundle' do + expect do + described_class.check!(invalid_bundle_path) + end.to raise_error(described_class::InvalidBundleError) + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 852ee9c96af..a19e3e84f83 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1753,22 +1753,23 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#create_from_bundle' do - let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:valid_bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:malicious_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') } let(:project) { create(:project) } let(:imported_repo) { project.repository.raw } before do - expect(repository.bundle_to_disk(bundle_path)).to be_truthy + expect(repository.bundle_to_disk(valid_bundle_path)).to be_truthy end after do - FileUtils.rm_rf(bundle_path) + FileUtils.rm_rf(valid_bundle_path) end it 'creates a repo from a bundle file' do expect(imported_repo).not_to exist - result = imported_repo.create_from_bundle(bundle_path) + result = imported_repo.create_from_bundle(valid_bundle_path) expect(result).to be_truthy expect(imported_repo).to exist @@ -1776,11 +1777,17 @@ describe Gitlab::Git::Repository, :seed_helper do end it 'creates a symlink to the global hooks dir' do - imported_repo.create_from_bundle(bundle_path) + imported_repo.create_from_bundle(valid_bundle_path) hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') } expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) end + + it 'raises an error if the bundle is an attempted malicious payload' do + expect do + imported_repo.create_from_bundle(malicious_bundle_path) + end.to raise_error(::Gitlab::Git::BundleFile::InvalidBundleError) + end end describe '#checksum' do diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb index c6f09ca2112..1ff2334bacf 100644 --- a/spec/lib/gitlab/gon_helper_spec.rb +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -29,4 +29,13 @@ describe Gitlab::GonHelper do helper.push_frontend_feature_flag(:my_feature_flag, 10) end end + + describe '#default_avatar_url' do + it 'returns an absolute URL' do + url = helper.default_avatar_url + + expect(url).to match(/^http/) + expect(url).to match(/no_avatar.*png$/) + end + end end diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index bdb1f34d2f6..24d49a049b6 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -101,16 +101,36 @@ describe Gitlab::Middleware::ReadOnly do expect(subject).not_to disallow_request end - it 'expects requests to sidekiq admin to be allowed' do - response = request.post('/admin/sidekiq') + context 'sidekiq admin requests' do + where(:mounted_at) do + [ + '', + '/', + '/gitlab', + '/gitlab/', + '/gitlab/gitlab', + '/gitlab/gitlab/' + ] + end - expect(response).not_to be_redirect - expect(subject).not_to disallow_request + with_them do + before do + stub_config_setting(relative_url_root: mounted_at) + end - response = request.get('/admin/sidekiq') + it 'allows requests' do + path = File.join(mounted_at, 'admin/sidekiq') + response = request.post(path) - expect(response).not_to be_redirect - expect(subject).not_to disallow_request + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + + response = request.get(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end end where(:description, :path) do diff --git a/spec/lib/gitlab/release_blog_post_spec.rb b/spec/lib/gitlab/release_blog_post_spec.rb new file mode 100644 index 00000000000..2c987df3767 --- /dev/null +++ b/spec/lib/gitlab/release_blog_post_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::ReleaseBlogPost do + describe '.blog_post_url' do + let(:releases_xml) do + <<~EOS + <?xml version='1.0' encoding='utf-8' ?> + <feed xmlns='http://www.w3.org/2005/Atom'> + <entry> + <release>11.2</release> + <id>https://about.gitlab.com/2018/08/22/gitlab-11-2-released/</id> + </entry> + <entry> + <release>11.1</release> + <id>https://about.gitlab.com/2018/07/22/gitlab-11-1-released/</id> + </entry> + <entry> + <release>11.0</release> + <id>https://about.gitlab.com/2018/06/22/gitlab-11-0-released/</id> + </entry> + <entry> + <release>10.8</release> + <id>https://about.gitlab.com/2018/05/22/gitlab-10-8-released/</id> + </entry> + </feed> + EOS + end + + subject { described_class.send(:new).blog_post_url } + + before do + stub_request(:get, 'https://about.gitlab.com/releases.xml') + .to_return(status: 200, headers: { 'content-type' => ['text/xml'] }, body: releases_xml) + end + + context 'matches GitLab version to blog post url' do + it 'returns the correct url for major pre release' do + stub_const('Gitlab::VERSION', '11.0.0-pre') + + expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/') + end + + it 'returns the correct url for major release candidate' do + stub_const('Gitlab::VERSION', '11.0.0-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/') + end + + it 'returns the correct url for major release' do + stub_const('Gitlab::VERSION', '11.0.0') + + expect(subject).to eql('https://about.gitlab.com/2018/06/22/gitlab-11-0-released/') + end + + it 'returns the correct url for minor pre release' do + stub_const('Gitlab::VERSION', '11.2.0-pre') + + expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/') + end + + it 'returns the correct url for minor release candidate' do + stub_const('Gitlab::VERSION', '11.2.0-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/') + end + + it 'returns the correct url for minor release' do + stub_const('Gitlab::VERSION', '11.2.0') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch pre release' do + stub_const('Gitlab::VERSION', '11.2.1-pre') + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch release candidate' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch release' do + stub_const('Gitlab::VERSION', '11.2.1') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns nil when no blog post is matched' do + stub_const('Gitlab::VERSION', '9.0.0') + + expect(subject).to be(nil) + end + end + end +end diff --git a/spec/lib/gitlab/tracing/factory_spec.rb b/spec/lib/gitlab/tracing/factory_spec.rb new file mode 100644 index 00000000000..945490f0988 --- /dev/null +++ b/spec/lib/gitlab/tracing/factory_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::Factory do + describe '.create_tracer' do + let(:service_name) { 'rspec' } + + context "when tracing is not configured" do + it 'ignores null connection strings' do + expect(described_class.create_tracer(service_name, nil)).to be_nil + end + + it 'ignores empty connection strings' do + expect(described_class.create_tracer(service_name, '')).to be_nil + end + + it 'ignores unknown implementations' do + expect(described_class.create_tracer(service_name, 'opentracing://invalid_driver')).to be_nil + end + + it 'ignores invalid connection strings' do + expect(described_class.create_tracer(service_name, 'open?tracing')).to be_nil + end + end + + context "when tracing is configured with jaeger" do + let(:mock_tracer) { double('tracer') } + + it 'processes default connections' do + expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer) + + expect(described_class.create_tracer(service_name, 'opentracing://jaeger')).to be(mock_tracer) + end + + it 'processes connections with parameters' do + expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: '1', b: '2', c: '3' }).and_return(mock_tracer) + + expect(described_class.create_tracer(service_name, 'opentracing://jaeger?a=1&b=2&c=3')).to be(mock_tracer) + end + end + end +end diff --git a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb new file mode 100644 index 00000000000..3d6a007cfd9 --- /dev/null +++ b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::JaegerFactory do + describe '.create_tracer' do + let(:service_name) { 'rspec' } + + shared_examples_for 'a jaeger tracer' do + it 'responds to active_span methods' do + expect(tracer).to respond_to(:active_span) + end + + it 'yields control' do + expect { |b| tracer.start_active_span('operation_name', &b) }.to yield_control + end + end + + context 'processes default connections' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, {}) } + end + end + + context 'handles debug options' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { debug: "1" }) } + end + end + + context 'handles const sampler' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { sampler: "const", sampler_param: "1" }) } + end + end + + context 'handles probabilistic sampler' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { sampler: "probabilistic", sampler_param: "0.5" }) } + end + end + + context 'handles http_endpoint configurations' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { http_endpoint: "http://localhost:1234" }) } + end + end + + context 'handles udp_endpoint configurations' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { udp_endpoint: "localhost:4321" }) } + end + end + + context 'ignores invalid parameters' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { invalid: "true" }) } + end + end + + context 'accepts the debug parameter when strict_parser is set' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { debug: "1", strict_parsing: "1" }) } + end + end + + it 'rejects invalid parameters when strict_parser is set' do + expect { described_class.create_tracer(service_name, { invalid: "true", strict_parsing: "1" }) }.to raise_error(StandardError) + end + end +end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index d63f448883b..6ac3d115bc6 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -8,6 +8,7 @@ describe Gitlab do expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__))) end end + describe '.revision' do let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] } @@ -69,6 +70,82 @@ describe Gitlab do end end + describe '.final_release?' do + subject { described_class.final_release? } + + context 'returns the corrent boolean value' do + it 'is false for a pre release' do + stub_const('Gitlab::VERSION', '11.0.0-pre') + + expect(subject).to be false + end + + it 'is false for a release candidate' do + stub_const('Gitlab::VERSION', '11.0.0-rc2') + + expect(subject).to be false + end + + it 'is true for a final release' do + stub_const('Gitlab::VERSION', '11.0.2') + + expect(subject).to be true + end + end + end + + describe '.minor_release' do + subject { described_class.minor_release } + + it 'returns the minor release of the full GitLab version' do + stub_const('Gitlab::VERSION', '11.0.1-rc3') + + expect(subject).to eql '11.0' + end + end + + describe '.previous_release' do + subject { described_class.previous_release } + + context 'it should return the previous release' do + it 'returns the previous major version when GitLab major version is not final' do + stub_const('Gitlab::VERSION', '11.0.1-pre') + + expect(subject).to eql '10' + end + + it 'returns the current minor version when the GitLab patch version is RC and > 0' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to eql '11.2' + end + + it 'returns the previous minor version when the GitLab patch version is RC and 0' do + stub_const('Gitlab::VERSION', '11.2.0-rc3') + + expect(subject).to eql '11.1' + end + end + end + + describe '.new_major_release?' do + subject { described_class.new_major_release? } + + context 'returns the corrent boolean value' do + it 'is true when the minor version is 0 and the patch is a pre release' do + stub_const('Gitlab::VERSION', '11.0.1-pre') + + expect(subject).to be true + end + + it 'is false when the minor version is above 0' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to be false + end + end + end + describe '.com?' do it 'is true when on GitLab.com' do stub_config_setting(url: 'https://gitlab.com') diff --git a/spec/migrations/README.md b/spec/migrations/README.md index 49760fa62b8..5df44dbc355 100644 --- a/spec/migrations/README.md +++ b/spec/migrations/README.md @@ -22,39 +22,33 @@ migrate the database **down** to the previous migration version. With this approach you can test a migration against a database schema that this migration has been written for. -Use `migrate!` helper to run the migration that is under test. - The `after` hook will migrate the database **up** and reinstitutes the latest schema version, so that the process does not affect subsequent specs and ensures proper isolation. -## Testing a class that is not an ActiveRecord::Migration - -In order to test a class that is not a migration itself, you will need to -manually provide a required schema version. Please add a `schema` tag to a -context that you want to switch the database schema within. - -Example: `describe SomeClass, :migration, schema: 20170608152748`. - ## Available helpers Use `table` helper to create a temporary `ActiveRecord::Base` derived model for a table. -Use `migrate!` helper to run the migration that is under test. It will not only +See `spec/support/helpers/migrations_helpers.rb` for all the available helpers. + +## Testing a class that is an ActiveRecord::Migration + +In order to test a class that is an `ActiveRecord::Migration`, you will need to +manually `require` the migration file because it is not autoloaded with Rails. + +Use `migrate!` helper to run the migration that is under test. It will not only run migration, but will also bump the schema version in the `schema_migrations` table. It is necessary because in the `after` hook we trigger the rest of the migrations, and we need to know where to start. -See `spec/support/migrations_helpers.rb` for all the available helpers. +### Example -## An example +This spec tests the [`db/post_migrate/20170526185842_migrate_pipeline_stages.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/db/post_migrate/20170526185842_migrate_pipeline_stages.rb) migration. You can find the complete spec on [`spec/migrations/migrate_pipeline_stages_spec.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/spec/migrations/migrate_pipeline_stages_spec.rb). ```ruby require 'spec_helper' - -# Load a migration class. - require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb') describe MigratePipelineStages, :migration do @@ -86,6 +80,56 @@ describe MigratePipelineStages, :migration do end ``` +## Testing a class that is not an ActiveRecord::Migration + +To test a class that is not an `ActiveRecord::Migration` (a background migration), +you will need to manually provide a required schema version. Please add a +schema tag to a context that you want to switch the database schema within. + +Example: `describe SomeClass, :migration, schema: 20170608152748`. + +### Example + +This spec tests the [`lib/gitlab/background_migration/archive_legacy_traces.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/lib/gitlab/background_migration/archive_legacy_traces.rb) +background migration. You can find the complete spec on +[`spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb) + +```ruby +require 'spec_helper' + +describe Gitlab::BackgroundMigration::ArchiveLegacyTraces, :migration, schema: 20180529152628 do + include TraceHelpers + + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:builds) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + @build = builds.create!(id: 1, project_id: 123, status: 'success', type: 'Ci::Build') + end + + context 'when trace file exsits at the right place' do + before do + create_legacy_trace(@build, 'trace in file') + end + + it 'correctly archive legacy traces' do + expect(job_artifacts.count).to eq(0) + expect(File.exist?(legacy_trace_path(@build))).to be_truthy + + described_class.new.perform(1, 1) + + expect(job_artifacts.count).to eq(1) + expect(File.exist?(legacy_trace_path(@build))).to be_falsy + expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in file') + end + end +end +``` + ## Best practices 1. Note that this type of tests do not run within the transaction, we use diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb index bf2fa5c0f56..efd87173b9c 100644 --- a/spec/migrations/add_foreign_keys_to_todos_spec.rb +++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb @@ -3,9 +3,11 @@ require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_tod describe AddForeignKeysToTodos, :migration do let(:todos) { table(:todos) } + let(:users) { table(:users) } + let(:projects) { table(:projects) } - let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs - let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:project) { projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) } + let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) } context 'add foreign key on user_id' do let!(:todo_with_user) { create_todo(user_id: user.id) } diff --git a/spec/migrations/cleanup_legacy_artifact_migration_spec.rb b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb new file mode 100644 index 00000000000..dc269d32e5a --- /dev/null +++ b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20190104182041_cleanup_legacy_artifact_migration.rb') + +describe CleanupLegacyArtifactMigration, :migration, :sidekiq, :redis do + let(:migration) { spy('migration') } + + context 'when still legacy artifacts exist' do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:pipelines) { table(:ci_pipelines) } + let(:jobs) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create!(name: 'gitlab', path: 'gitlab-ce', namespace_id: namespace.id) } + let(:pipeline) { pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a') } + let(:archive_file_type) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::ARCHIVE_FILE_TYPE } + let(:metadata_file_type) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::METADATA_FILE_TYPE } + let(:local_store) { ::ObjectStorage::Store::LOCAL } + let(:remote_store) { ::ObjectStorage::Store::REMOTE } + let(:legacy_location) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::LEGACY_PATH_FILE_LOCATION } + + before do + jobs.create!(id: 1, commit_id: pipeline.id, project_id: project.id, status: :success, artifacts_file: 'archive.zip') + jobs.create!(id: 2, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_metadata: 'metadata.gz') + jobs.create!(id: 3, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_file: 'archive.zip', artifacts_metadata: 'metadata.gz') + jobs.create!(id: 4, commit_id: pipeline.id, project_id: project.id, status: :running) + jobs.create!(id: 5, commit_id: pipeline.id, project_id: project.id, status: :success, artifacts_file: 'archive.zip', artifacts_file_store: remote_store, artifacts_metadata: 'metadata.gz') + jobs.create!(id: 6, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_file: 'archive.zip', artifacts_metadata: 'metadata.gz') + end + + it 'steals sidekiq jobs from MigrateLegacyArtifacts background migration' do + expect(Gitlab::BackgroundMigration).to receive(:steal).with('MigrateLegacyArtifacts') + + migrate! + end + + it 'migrates legacy artifacts to ci_job_artifacts table' do + migrate! + + expect(job_artifacts.order(:job_id, :file_type).pluck('project_id, job_id, file_type, file_store, size, expire_at, file, file_sha256, file_location')) + .to eq([[project.id, 1, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 3, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 3, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location], + [project.id, 5, archive_file_type, remote_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 5, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location], + [project.id, 6, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 6, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location]]) + end + end +end diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb index b5980cb9ddb..651341906c2 100644 --- a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb +++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb') describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222043024 do + let(:projects) { table(:projects) } + before do # Stub after_save callbacks that will fail when Project has no namespace allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil) @@ -10,9 +12,9 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 describe '#up' do it 'only cleans up pending delete projects' do - create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs - create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs - project = build(:project, pending_delete: true, namespace_id: nil) # rubocop:disable RSpec/FactoriesInMigrationSpecs + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ee', namespace_id: 2, pending_delete: true) + project = Project.new(pending_delete: true, namespace_id: nil) project.save(validate: false) expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]]) @@ -21,8 +23,8 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 end it 'does nothing when no pending delete projects without namespace found' do - create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs - create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ee', namespace_id: 2, pending_delete: true) expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 1afc2436bb5..60d89313f07 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3028,6 +3028,24 @@ describe Ci::Build do subject.drop! end end + + context 'when associated deployment failed to update its status' do + let(:build) { create(:ci_build, :running, pipeline: pipeline) } + let!(:deployment) { create(:deployment, deployable: build) } + + before do + allow_any_instance_of(Deployment) + .to receive(:drop!).and_raise('Unexpected error') + end + + it 'can drop the build' do + expect(Gitlab::Sentry).to receive(:track_exception) + + expect { build.drop! }.not_to raise_error + + expect(build).to be_failed + end + end end describe '.matches_tag_ids' do diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index de313a8ca36..52c347229c6 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -35,7 +35,7 @@ describe Clusters::Applications::Ingress do let(:application) { create(:clusters_applications_ingress, :scheduled, version: '0.22.0') } it 'updates the application version' do - expect(application.reload.version).to eq('0.23.0') + expect(application.reload.version).to eq('1.1.2') end end end @@ -90,7 +90,7 @@ describe Clusters::Applications::Ingress do it 'should be initialized with ingress arguments' do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') - expect(subject.version).to eq('0.23.0') + expect(subject.version).to eq('1.1.2') expect(subject).to be_rbac expect(subject.files).to eq(ingress.files) end @@ -107,7 +107,7 @@ describe Clusters::Applications::Ingress do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } it 'should be initialized with the locked version' do - expect(subject.version).to eq('0.23.0') + expect(subject.version).to eq('1.1.2') end end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index de6b844023a..e50ba67c493 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -90,6 +90,24 @@ describe Clusters::Applications::Prometheus do expect(application).not_to be_ready end + + it 'returns true when updating' do + application = build(:clusters_applications_prometheus, :updating, cluster: cluster) + + expect(application).to be_ready + end + + it 'returns true when updated' do + application = build(:clusters_applications_prometheus, :updated, cluster: cluster) + + expect(application).to be_ready + end + + it 'returns true when errored' do + application = build(:clusters_applications_prometheus, :update_errored, cluster: cluster) + + expect(application).to be_ready + end end describe '#prometheus_client' do @@ -197,6 +215,46 @@ describe Clusters::Applications::Prometheus do end end + describe '#upgrade_command' do + let(:prometheus) { build(:clusters_applications_prometheus) } + let(:values) { prometheus.values } + + it 'returns an instance of Gitlab::Kubernetes::Helm::GetCommand' do + expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::UpgradeCommand) + end + + it 'should be initialized with 3 arguments' do + command = prometheus.upgrade_command(values) + + expect(command.name).to eq('prometheus') + expect(command.chart).to eq('stable/prometheus') + expect(command.version).to eq('6.7.3') + expect(command.files).to eq(prometheus.files) + end + end + + describe '#update_in_progress?' do + context 'when app is updating' do + it 'returns true' do + cluster = create(:cluster) + prometheus_app = build(:clusters_applications_prometheus, :updating, cluster: cluster) + + expect(prometheus_app.update_in_progress?).to be true + end + end + end + + describe '#update_errored?' do + context 'when app errored' do + it 'returns true' do + cluster = create(:cluster) + prometheus_app = build(:clusters_applications_prometheus, :update_errored, cluster: cluster) + + expect(prometheus_app.update_errored?).to be true + end + end + end + describe '#files' do let(:application) { create(:clusters_applications_prometheus) } let(:values) { subject[:'values.yaml'] } @@ -211,4 +269,43 @@ describe Clusters::Applications::Prometheus do expect(values).to include('serverFiles') end end + + describe '#files_with_replaced_values' do + let(:application) { build(:clusters_applications_prometheus) } + let(:files) { application.files } + + subject { application.files_with_replaced_values({ hello: :world }) } + + it 'does not modify #files' do + expect(subject[:'values.yaml']).not_to eq(files) + expect(files[:'values.yaml']).to eq(application.values) + end + + it 'returns values.yaml with replaced values' do + expect(subject[:'values.yaml']).to eq({ hello: :world }) + end + + it 'should include cert files' do + expect(subject[:'ca.pem']).to be_present + expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) + + expect(subject[:'cert.pem']).to be_present + expect(subject[:'key.pem']).to be_present + + cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) + expect(cert.not_after).to be < 60.minutes.from_now + end + + context 'when the helm application does not have a ca_cert' do + before do + application.cluster.application_helm.ca_cert = nil + end + + it 'should not include cert files' do + expect(subject[:'ca.pem']).not_to be_present + expect(subject[:'cert.pem']).not_to be_present + expect(subject[:'key.pem']).not_to be_present + end + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index e63881242f6..9dc32a815d8 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -722,6 +722,42 @@ describe Group do end end + describe '#highest_group_member', :nested_groups do + let(:nested_group) { create(:group, parent: group) } + let(:nested_group_2) { create(:group, parent: nested_group) } + let(:user) { create(:user) } + + subject(:highest_group_member) { nested_group_2.highest_group_member(user) } + + context 'when the user is not a member of any group in the hierarchy' do + it 'returns nil' do + expect(highest_group_member).to be_nil + end + end + + context 'when the user is only a member of one group in the hierarchy' do + before do + nested_group.add_developer(user) + end + + it 'returns that group member' do + expect(highest_group_member.access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when the user is a member of several groups in the hierarchy' do + before do + group.add_owner(user) + nested_group.add_developer(user) + nested_group_2.add_maintainer(user) + end + + it 'returns the group member with the highest access level' do + expect(highest_group_member.access_level).to eq(Gitlab::Access::OWNER) + end + end + end + describe '#has_parent?' do context 'when the group has a parent' do it 'should be truthy' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 015db4d4e96..2e436f2cc8a 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -286,8 +286,8 @@ describe Milestone do end context 'relations as params' do - let(:projects) { Project.where(id: project.id) } - let(:groups) { Group.where(id: group.id) } + let(:projects) { Project.where(id: project.id).select(:id) } + let(:groups) { Group.where(id: group.id).select(:id) } it_behaves_like 'filters by projects and groups' end diff --git a/spec/models/project_import_data_spec.rb b/spec/models/project_import_data_spec.rb index e9910c0a5d1..fe47811f074 100644 --- a/spec/models/project_import_data_spec.rb +++ b/spec/models/project_import_data_spec.rb @@ -39,4 +39,15 @@ describe ProjectImportData do expect(row.credentials).to eq({ 'number' => 10, 'foo' => 'bar' }) end end + + describe '#clear_credentials' do + it 'clears out the Hash' do + row = described_class.new + + row.merge_credentials('number' => 10) + row.clear_credentials + + expect(row.credentials).to eq({}) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 397b4d7c61f..7a8dc59039e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -209,9 +209,14 @@ describe Project do it 'does not allow new projects beyond user limits' do project2 = build(:project) - allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) + + allow(project2) + .to receive(:creator) + .and_return( + double(can_create_project?: false, projects_limit: 0).as_null_object + ) + expect(project2).not_to be_valid - expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/) end describe 'wiki path conflict' do @@ -3087,7 +3092,7 @@ describe Project do context 'when the project is in a subgroup' do let(:group) { create(:group, :nested) } - it { is_expected.to be(false) } + it { is_expected.to be(true) } end end @@ -4431,6 +4436,75 @@ describe Project do end end + describe '#check_personal_projects_limit' do + context 'when creating a project for a group' do + it 'does nothing' do + creator = build(:user) + project = build(:project, namespace: build(:group), creator: creator) + + allow(creator) + .to receive(:can_create_project?) + .and_return(false) + + project.check_personal_projects_limit + + expect(project.errors).to be_empty + end + end + + context 'when the user is not allowed to create a personal project' do + let(:user) { build(:user) } + let(:project) { build(:project, creator: user) } + + before do + allow(user) + .to receive(:can_create_project?) + .and_return(false) + end + + context 'when the project limit is zero' do + it 'adds a validation error' do + allow(user) + .to receive(:projects_limit) + .and_return(0) + + project.check_personal_projects_limit + + expect(project.errors[:limit_reached].first) + .to match(/Personal project creation is not allowed/) + end + end + + context 'when the project limit is greater than zero' do + it 'adds a validation error' do + allow(user) + .to receive(:projects_limit) + .and_return(5) + + project.check_personal_projects_limit + + expect(project.errors[:limit_reached].first) + .to match(/Your project limit is 5 projects/) + end + end + end + + context 'when the user is allowed to create personal projects' do + it 'does nothing' do + user = build(:user) + project = build(:project, creator: user) + + allow(user) + .to receive(:can_create_project?) + .and_return(true) + + project.check_personal_projects_limit + + expect(project.errors).to be_empty + end + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 224bc9ed935..c06e9a08ab4 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -303,6 +303,25 @@ describe RemoteMirror, :mailer do end end + context '#url=' do + let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + + it 'resets all the columns when URL changes' do + remote_mirror.update(last_error: Time.now, + last_update_at: Time.now, + last_successful_update_at: Time.now, + update_status: 'started', + error_notification_sent: true) + + expect { remote_mirror.update_attribute(:url, 'http://new.example.com') } + .to change { remote_mirror.last_error }.to(nil) + .and change { remote_mirror.last_update_at }.to(nil) + .and change { remote_mirror.last_successful_update_at }.to(nil) + .and change { remote_mirror.update_status }.to('finished') + .and change { remote_mirror.error_notification_sent }.to(false) + end + end + context '#updated_since?' do let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } let(:timestamp) { Time.now - 5.minutes } diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 7d3eff7d32d..22a9e36ca31 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -129,6 +129,40 @@ describe API::Features do end end + context 'when enabling for a project by path' do + context 'when the project exists' do + let!(:project) { create(:project) } + + it 'sets the feature gate' do + post api("/features/#{feature_name}", admin), params: { value: 'true', project: project.full_path } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["Project:#{project.id}"] } + ]) + end + end + + context 'when the project does not exist' do + it 'sets no new values' do + post api("/features/#{feature_name}", admin), params: { value: 'true', project: 'mep/to/the/mep/mep' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + "name" => "my_feature", + "state" => "off", + "gates" => [ + { "key" => "boolean", "value" => false } + ] + ) + end + end + end + it 'creates a feature with the given percentage if passed an integer' do post api("/features/#{feature_name}", admin), params: { value: '50' } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index c9dfc5c4a7e..7176bc23e34 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -382,6 +382,20 @@ describe API::Groups do expect(response_project_ids(json_response, 'shared_projects')) .to contain_exactly(projects[:public].id, projects[:internal].id) end + + it 'avoids N+1 queries' do + get api("/groups/#{group1.id}", admin) + + control_count = ActiveRecord::QueryRecorder.new do + get api("/groups/#{group1.id}", admin) + end.count + + create(:project, namespace: group1) + + expect do + get api("/groups/#{group1.id}", admin) + end.not_to exceed_query_limit(control_count) + end end context "when authenticated as admin" do diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb new file mode 100644 index 00000000000..aceff9b4aa6 --- /dev/null +++ b/spec/requests/api/import_github_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe API::ImportGithub do + include ApiHelpers + + let(:token) { "asdasd12345" } + let(:provider) { :github } + let(:access_params) { { github_access_token: token } } + + describe "POST /import/github" do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:provider_username) { user.username } + let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_repo) do + OpenStruct.new( + name: 'vim', + full_name: "#{provider_username}/vim", + owner: OpenStruct.new(login: provider_username) + ) + end + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repo: provider_repo).as_null_object) + end + end + + it 'returns 201 response when the project is imported successfully' do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + + post api("/import/github", user), params: { + target_namespace: user.namespace_path, + personal_access_token: token, + repo_id: 1234 + } + expect(response).to have_gitlab_http_status(201) + expect(json_response).to be_a Hash + expect(json_response['name']).to eq(project.name) + end + + it 'returns 422 response when user can not create projects in the chosen namespace' do + other_namespace = create(:group, name: 'other_namespace') + + post api("/import/github", user), params: { + target_namespace: other_namespace.name, + personal_access_token: token, + repo_id: 1234 + } + + expect(response).to have_gitlab_http_status(422) + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index ffe4512fa6f..7248908b494 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -948,6 +948,7 @@ describe API::Projects do expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['shared_with_groups'][0]['expires_at']).to be_nil expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) @@ -967,6 +968,7 @@ describe API::Projects do expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['shared_with_groups'][0]['expires_at']).to eq(expires_at.to_s) end @@ -1145,6 +1147,40 @@ describe API::Projects do .to eq(Gitlab::Access::OWNER) end end + + context 'nested group project', :nested_groups do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:project2) { create(:project, group: nested_group) } + + before do + project2.group.parent.add_owner(user) + end + + it 'sets group access and return 200' do + get api("/projects/#{project2.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']) + .to eq(Gitlab::Access::OWNER) + end + + context 'with various access levels across nested groups' do + before do + project2.group.add_maintainer(user) + end + + it 'sets the maximum group access and return 200' do + get api("/projects/#{project2.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']) + .to eq(Gitlab::Access::OWNER) + end + end + end end end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index cfbda63bb30..45fb1562e84 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -63,7 +63,8 @@ describe API::Settings, 'Settings' do terms: 'Hello world!', performance_bar_allowed_group_path: group.full_path, instance_statistics_visibility_private: true, - diff_max_patch_bytes: 150_000 + diff_max_patch_bytes: 150_000, + default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE } expect(response).to have_gitlab_http_status(200) @@ -88,6 +89,7 @@ describe API::Settings, 'Settings' do expect(json_response['performance_bar_allowed_group_id']).to eq(group.id) expect(json_response['instance_statistics_visibility_private']).to be(true) expect(json_response['diff_max_patch_bytes']).to eq(150_000) + expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) end end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index d09b6fe72b1..fffe878ddbd 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -54,6 +54,18 @@ describe API::Tags do end end + context 'searching' do + it 'only returns searched tags' do + get api("#{route}", user), params: { search: 'v1.1.0' } + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response[0]['name']).to eq('v1.1.0') + end + end + shared_examples_for 'repository tags' do it 'returns the repository tags' do get api(route, current_user) diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index f5092e8e2b5..6109829aad1 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -22,7 +22,7 @@ describe API::Wikis do context 'when wiki has pages' do let!(:pages) do [create(:wiki_page, wiki: project_wiki, attrs: { title: 'page1', content: 'content of page1' }), - create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2', content: 'content of page2' })] + create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2.with.dot', content: 'content of page2' })] end it 'returns the list of wiki pages without content' do diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb index 28cb90e450e..c63fbcdd84e 100644 --- a/spec/requests/lfs_locks_api_spec.rb +++ b/spec/requests/lfs_locks_api_spec.rb @@ -132,6 +132,17 @@ describe 'Git LFS File Locking API' do expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) end + + context 'when a maintainer uses force' do + let(:authorization) { authorize_user(maintainer) } + + it 'deletes the lock' do + project.add_maintainer(maintainer) + post_lfs_json url, { force: true }, headers + + expect(response).to have_gitlab_http_status(200) + end + end end end diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb index 08ffc3c3a53..0ff777388e5 100644 --- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb +++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb @@ -19,6 +19,41 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do SOURCE end + it 'does not flag the use of `prepend EEFoo` in the middle of a file' do + expect_no_offenses(<<~SOURCE) + class Foo + prepend EEFoo + end + SOURCE + end + + it 'flags the use of `prepend EE::Foo::Bar` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend EE::Foo::Bar + ^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + + it 'flags the use of `prepend(EE::Foo::Bar)` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend(EE::Foo::Bar) + ^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + + it 'flags the use of `prepend EE::Foo::Bar::Baz` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend EE::Foo::Bar::Baz + ^^^^^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + it 'flags the use of `prepend ::EE` in the middle of a file' do expect_offense(<<~SOURCE) class Foo diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 12ddf8447bd..dfbdfa2ab69 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -281,6 +281,40 @@ describe Projects::DestroyService do end end + context 'repository +deleted path removal' do + def removal_path(path) + "#{path}+#{project.id}#{described_class::DELETED_FLAG}" + end + + context 'regular phase' do + it 'schedules +deleted removal of existing repos' do + service = described_class.new(project, user, {}) + allow(service).to receive(:schedule_stale_repos_removal) + + expect(GitlabShellWorker).to receive(:perform_in) + .with(5.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path)) + + service.execute + end + end + + context 'stale cleanup' do + let!(:async) { true } + + it 'schedules +deleted wiki and repo removal' do + allow(ProjectDestroyWorker).to receive(:perform_async) + + expect(GitlabShellWorker).to receive(:perform_in) + .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path)) + + expect(GitlabShellWorker).to receive(:perform_in) + .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.wiki.disk_path)) + + destroy_project(project, user, {}) + end + end + end + context '#attempt_restore_repositories' do let(:path) { project.disk_path + '.git' } diff --git a/spec/services/projects/protect_default_branch_service_spec.rb b/spec/services/projects/protect_default_branch_service_spec.rb new file mode 100644 index 00000000000..c145b2c06c6 --- /dev/null +++ b/spec/services/projects/protect_default_branch_service_spec.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::ProtectDefaultBranchService do + let(:service) { described_class.new(project) } + let(:project) { instance_spy(Project) } + + describe '#execute' do + before do + allow(service) + .to receive(:protect_default_branch) + end + + context 'without a default branch' do + it 'does nothing' do + allow(service) + .to receive(:default_branch) + .and_return(nil) + + service.execute + + expect(service) + .not_to have_received(:protect_default_branch) + end + end + + context 'with a default branch' do + it 'protects the default branch' do + allow(service) + .to receive(:default_branch) + .and_return('master') + + service.execute + + expect(service) + .to have_received(:protect_default_branch) + end + end + end + + describe '#protect_default_branch' do + before do + allow(service) + .to receive(:default_branch) + .and_return('master') + + allow(project) + .to receive(:change_head) + .with('master') + + allow(service) + .to receive(:create_protected_branch) + end + + context 'when branch protection is needed' do + before do + allow(service) + .to receive(:protect_branch?) + .and_return(true) + + allow(service) + .to receive(:create_protected_branch) + end + + it 'changes the HEAD of the project' do + service.protect_default_branch + + expect(project) + .to have_received(:change_head) + end + + it 'protects the default branch' do + service.protect_default_branch + + expect(service) + .to have_received(:create_protected_branch) + end + end + + context 'when branch protection is not needed' do + before do + allow(service) + .to receive(:protect_branch?) + .and_return(false) + end + + it 'changes the HEAD of the project' do + service.protect_default_branch + + expect(project) + .to have_received(:change_head) + end + + it 'does not protect the default branch' do + service.protect_default_branch + + expect(service) + .not_to have_received(:create_protected_branch) + end + end + end + + describe '#create_protected_branch' do + it 'creates the protected branch' do + creator = instance_spy(User) + create_service = instance_spy(ProtectedBranches::CreateService) + access_level = Gitlab::Access::DEVELOPER + params = { + name: 'master', + push_access_levels_attributes: [{ access_level: access_level }], + merge_access_levels_attributes: [{ access_level: access_level }] + } + + allow(project) + .to receive(:creator) + .and_return(creator) + + allow(ProtectedBranches::CreateService) + .to receive(:new) + .with(project, creator, params) + .and_return(create_service) + + allow(service) + .to receive(:push_access_level) + .and_return(access_level) + + allow(service) + .to receive(:merge_access_level) + .and_return(access_level) + + allow(service) + .to receive(:default_branch) + .and_return('master') + + allow(create_service) + .to receive(:execute) + .with(skip_authorization: true) + + service.create_protected_branch + + expect(create_service) + .to have_received(:execute) + end + end + + describe '#protect_branch?' do + context 'when default branch protection is disabled' do + it 'returns false' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_NONE) + + expect(service.protect_branch?).to eq(false) + end + end + + context 'when default branch protection is enabled' do + before do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + allow(service) + .to receive(:default_branch) + .and_return('master') + end + + it 'returns false if the branch is already protected' do + allow(ProtectedBranch) + .to receive(:protected?) + .with(project, 'master') + .and_return(true) + + expect(service.protect_branch?).to eq(false) + end + + it 'returns true if the branch is not yet protected' do + allow(ProtectedBranch) + .to receive(:protected?) + .with(project, 'master') + .and_return(false) + + expect(service.protect_branch?).to eq(true) + end + end + end + + describe '#default_branch' do + it 'returns the default branch of the project' do + allow(project) + .to receive(:default_branch) + .and_return('master') + + expect(service.default_branch).to eq('master') + end + end + + describe '#push_access_level' do + context 'when developers can push' do + it 'returns the DEVELOPER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(service.push_access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when developers can not push' do + it 'returns the MAINTAINER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(service.push_access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + end + + describe '#merge_access_level' do + context 'when developers can merge' do + it 'returns the DEVELOPER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(service.merge_access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when developers can not merge' do + it 'returns the MAINTAINER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(service.merge_access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + end +end diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index 3a483717756..e5ca1c155ed 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -117,13 +117,184 @@ describe Suggestions::ApplyService do expect(commit.committer_email).to eq(user.commit_email) expect(commit.author_name).to eq(user.name) end + + context 'when it fails to apply because the file was changed' do + it 'returns error message' do + service = instance_double(Files::UpdateService) + + expect(Files::UpdateService).to receive(:new) + .and_return(service) + + allow(service).to receive(:execute) + .and_raise(Files::UpdateService::FileChangedError) + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'The file has been changed', status: :error) + end + end + + context 'when diff ref from position is different from repo diff refs' do + it 'returns error message' do + outdated_refs = Gitlab::Diff::DiffRefs.new(base_sha: 'foo', start_sha: 'bar', head_sha: 'outdated') + + allow(suggestion).to receive(:appliable?) { true } + allow(suggestion.position).to receive(:diff_refs) { outdated_refs } + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'The file has been changed', status: :error) + end + end + + context 'multiple suggestions applied' do + let(:expected_content) do + <<-CONTENT.strip_heredoc + require 'fileutils' + require 'open3' + + module Popen + extend self + + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) + # v1 change + end + + path ||= Dir.pwd + # v1 change + vars = { + "PWD" => path + } + + options = { + chdir: path + } + # v2 change + unless File.directory?(path) + FileUtils.mkdir_p(path) + end + + @cmd_output = "" + # v2 change + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + @cmd_output << stderr.read + @cmd_status = wait_thr.value.exitstatus + end + + return @cmd_output, @cmd_status + end + end + CONTENT + end + + let(:merge_request) do + create(:merge_request, source_project: project, + target_project: project) + end + + def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:) + position = Gitlab::Diff::Position.new(old_path: path, + new_path: path, + old_line: old_line, + new_line: new_line, + diff_refs: diff.diff_refs) + + suggestion_note = create(:diff_note_on_merge_request, noteable: merge_request, + original_position: position, + position: position, + project: project) + create(:suggestion, note: suggestion_note, + from_content: from_content, + to_content: to_content) + end + + def apply_suggestion(suggestion) + suggestion.note.reload + merge_request.reload + merge_request.clear_memoized_shas + + result = subject.execute(suggestion) + refresh = MergeRequests::RefreshService.new(project, user) + refresh.execute(merge_request.diff_head_sha, + suggestion.commit_id, + merge_request.source_branch_ref) + + result + end + + def fetch_raw_diff(suggestion) + project.reload.commit(suggestion.commit_id).diffs.diff_files.first.diff.diff + end + + it 'applies multiple suggestions in subsequent versions correctly' do + diff = merge_request.merge_request_diff + path = 'files/ruby/popen.rb' + + suggestion_1_changes = { old_line: nil, + new_line: 13, + from_content: "\n", + to_content: "# v1 change\n", + path: path } + + suggestion_2_changes = { old_line: 24, + new_line: 31, + from_content: " @cmd_output << stderr.read\n", + to_content: "# v2 change\n", + path: path } + + suggestion_1 = create_suggestion(diff, suggestion_1_changes) + suggestion_2 = create_suggestion(diff, suggestion_2_changes) + + apply_suggestion(suggestion_1) + + suggestion_1_diff = fetch_raw_diff(suggestion_1) + + # rubocop: disable Layout/TrailingWhitespace + expected_suggestion_1_diff = <<-CONTENT.strip_heredoc + @@ -10,7 +10,7 @@ module Popen + end + + path ||= Dir.pwd + - + +# v1 change + vars = { + "PWD" => path + } + CONTENT + # rubocop: enable Layout/TrailingWhitespace + + apply_suggestion(suggestion_2) + + suggestion_2_diff = fetch_raw_diff(suggestion_2) + + # rubocop: disable Layout/TrailingWhitespace + expected_suggestion_2_diff = <<-CONTENT.strip_heredoc + @@ -28,7 +28,7 @@ module Popen + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + - @cmd_output << stderr.read + +# v2 change + @cmd_status = wait_thr.value.exitstatus + end + CONTENT + # rubocop: enable Layout/TrailingWhitespace + + expect(suggestion_1_diff.strip).to eq(expected_suggestion_1_diff.strip) + expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip) + end + end end context 'fork-project' do let(:project) { create(:project, :public, :repository) } let(:forked_project) do - fork_project_with_submodules(project, user) + fork_project_with_submodules(project, user, repository: project.repository) end let(:merge_request) do diff --git a/spec/sidekiq/cron/job_gem_dependency_spec.rb b/spec/sidekiq/cron/job_gem_dependency_spec.rb index 2e30cf025b0..2e7de75fd08 100644 --- a/spec/sidekiq/cron/job_gem_dependency_spec.rb +++ b/spec/sidekiq/cron/job_gem_dependency_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Sidekiq::Cron::Job do describe 'cron jobs' do - context 'when rufus-scheduler depends on ZoTime or EoTime' do + context 'when Fugit depends on ZoTime or EoTime' do before do described_class .create(name: 'TestCronWorker', @@ -10,7 +10,7 @@ describe Sidekiq::Cron::Job do class: Settings.cron_jobs[:pipeline_schedule_worker]['job_class']) end - it 'does not get "Rufus::Scheduler::ZoTime/EtOrbi::EoTime into an exact number"' do + it 'does not get any errors' do expect { described_class.all.first.should_enque?(Time.now) }.not_to raise_error end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 89357056c93..72684caad32 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,7 +22,7 @@ if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled) require 'rspec_profiling/rspec' end -if ENV['CI'] && !ENV['NO_KNAPSACK'] +if ENV['CI'] && ENV['KNAPSACK_GENERATE_REPORT'] && !ENV['NO_KNAPSACK'] require 'knapsack' Knapsack::Adapters::RSpecAdapter.bind end @@ -127,6 +127,16 @@ RSpec.configure do |config| .and_return(false) end + config.before(:suite) do + # Set latest release blog post URL for "What's new?" link + Gitlab::ReleaseBlogPost.instance.instance_variable_set(:@url, 'https://about.gitlab.com') + end + + config.before(:example, :quarantine) do + # Skip tests in quarantine unless we explicitly focus on them. + skip('In quarantine') unless config.inclusion_filter[:quarantine] + end + config.before(:example, :request_store) do RequestStore.begin! end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index dbdca99b5aa..0acc9e2a836 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -1,8 +1,16 @@ shared_examples 'issuable notes filter' do + let(:params) do + if issuable_parent.is_a?(Project) + { namespace_id: issuable_parent.namespace, project_id: issuable_parent, id: issuable.iid } + else + { group_id: issuable_parent, id: issuable.to_param } + end + end + it 'sets discussion filter' do notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) expect(user.reload.notes_filter_for(issuable)).to eq(notes_filter) expect(UserPreference.count).to eq(1) @@ -13,7 +21,7 @@ shared_examples 'issuable notes filter' do expect_any_instance_of(issuable.class).to receive(:expire_note_etag_cache) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) end it 'does not expires notes e-tag cache for issuable if filter did not change' do @@ -22,14 +30,14 @@ shared_examples 'issuable notes filter' do expect_any_instance_of(issuable.class).not_to receive(:expire_note_etag_cache) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) end it 'does not set notes filter when database is in read only mode' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) expect(user.reload.notes_filter_for(issuable)).to eq(0) end @@ -37,7 +45,7 @@ shared_examples 'issuable notes filter' do it 'returns only user comments' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid } + get :discussions, params: params discussions = JSON.parse(response.body) expect(discussions.count).to eq(1) @@ -47,7 +55,7 @@ shared_examples 'issuable notes filter' do it 'returns only activity notes' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid } + get :discussions, params: params discussions = JSON.parse(response.body) expect(discussions.count).to eq(1) @@ -60,7 +68,7 @@ shared_examples 'issuable notes filter' do expect(ResourceEvents::MergeIntoNotesService).not_to receive(:new) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid } + get :discussions, params: params end end end diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb index 1f688c0f9d3..dcf7c1a90c2 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb @@ -32,11 +32,13 @@ shared_examples 'backfill migration for project repositories' do |storage| it 'inserts rows in a single query' do projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + group2 = namespaces.create!(name: 'gro', path: 'gro') control_count = ActiveRecord::QueryRecorder.new { described_class.new.perform(1, projects.last.id) } projects.create!(name: 'bar', path: 'bar', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) - projects.create!(name: 'zoo', path: 'zoo', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + projects.create!(name: 'top', path: 'top', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + projects.create!(name: 'zoo', path: 'zoo', namespace_id: group2.id, storage_version: storage_version, repository_storage: shard.name) expect { described_class.new.perform(1, projects.last.id) }.not_to exceed_query_limit(control_count) end diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb index f30b5881fde..ceb7e34a540 100644 --- a/spec/views/help/instance_configuration.html.haml_spec.rb +++ b/spec/views/help/instance_configuration.html.haml_spec.rb @@ -13,9 +13,9 @@ describe 'help/instance_configuration' do it 'has links to several sections' do render - expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any? - expect(rendered).to have_link(nil, '#gitlab-pages') - expect(rendered).to have_link(nil, '#gitlab-ci') + expect(rendered).to have_link(nil, href: '#ssh-host-keys-fingerprints') if ssh_settings.any? + expect(rendered).to have_link(nil, href: '#gitlab-pages') + expect(rendered).to have_link(nil, href: '#gitlab-ci') end it 'has several sections' do diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb index a9c32122600..d07099489e5 100644 --- a/spec/views/projects/commit/show.html.haml_spec.rb +++ b/spec/views/projects/commit/show.html.haml_spec.rb @@ -54,9 +54,9 @@ describe 'projects/commit/show.html.haml' do end it 'shows that it is in the context of a merge request' do - merge_request_url = diffs_project_merge_request_url(project, merge_request, commit_id: commit.id) + merge_request_url = diffs_project_merge_request_path(project, merge_request, commit_id: commit.id) expect(rendered).to have_content("This commit is part of merge request") - expect(rendered).to have_link(merge_request.to_reference, merge_request_url) + expect(rendered).to have_link(merge_request.to_reference, href: merge_request_url) end end end diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb index 752fd82c5e8..8e34521c7c8 100644 --- a/spec/views/projects/settings/operations/show.html.haml_spec.rb +++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb @@ -13,8 +13,6 @@ describe 'projects/settings/operations/show' do describe 'Operations > Error Tracking' do before do - stub_feature_flags(error_tracking: true) - project.add_reporter(user) allow(view).to receive(:error_tracking_setting) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index a159f24f876..4895a968d6e 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -71,6 +71,17 @@ describe GitGarbageCollectWorker do subject.perform(project.id) end + + context 'when the repository has joined a pool' do + let!(:pool) { create(:pool_repository, :ready) } + let(:project) { pool.source_project } + + it 'ensures the repositories are linked' do + expect_any_instance_of(PoolRepository).to receive(:link_repository).once + + subject.perform(project.id) + end + end end context 'when no lease can be obtained' do diff --git a/spec/workers/remote_mirror_notification_worker_spec.rb b/spec/workers/remote_mirror_notification_worker_spec.rb new file mode 100644 index 00000000000..e3db10ed645 --- /dev/null +++ b/spec/workers/remote_mirror_notification_worker_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe RemoteMirrorNotificationWorker, :mailer do + set(:project) { create(:project, :repository, :remote_mirror) } + set(:mirror) { project.remote_mirrors.first } + + describe '#execute' do + it 'calls NotificationService#remote_mirror_update_failed when the mirror exists' do + mirror.update_column(:last_error, "There was a problem fetching") + + expect(NotificationService).to receive_message_chain(:new, :remote_mirror_update_failed) + + subject.perform(mirror.id) + + expect(mirror.reload.error_notification_sent?).to be_truthy + end + + it 'does nothing when the mirror has no errors' do + expect(NotificationService).not_to receive(:new) + + subject.perform(mirror.id) + end + + it 'does nothing when the mirror does not exist' do + expect(NotificationService).not_to receive(:new) + + subject.perform(RemoteMirror.maximum(:id).to_i.succ) + end + + it 'does nothing when a notification has already been sent' do + mirror.update_columns(last_error: "There was a problem fetching", + error_notification_sent: true) + + expect(NotificationService).not_to receive(:new) + + subject.perform(mirror.id) + end + end +end diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb index d73b0b53713..b582a3650b6 100644 --- a/spec/workers/repository_update_remote_mirror_worker_spec.rb +++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb @@ -22,6 +22,13 @@ describe RepositoryUpdateRemoteMirrorWorker do expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished') end + it 'resets the notification flag upon success' do + expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + remote_mirror.update_column(:error_notification_sent, true) + + expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.error_notification_sent }.to(false) + end + it 'sets status as failed when update remote mirror service executes with errors' do error_message = 'fail!' diff --git a/yarn.lock b/yarn.lock index da00f335362..7b3144fca16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,18 +29,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.1.2.tgz#fde75c072575ce7abbd97322e8fef5bae67e4630" - integrity sha512-70A9HWLS/1RHk3Ck8tNHKxOoKQuSKocYgwDN85Pyl/RBduss6AKxUR7RIZ/lzduQMSYfWEM4DDBu6A+XGbkFig== - dependencies: - "@babel/types" "^7.1.2" - jsesc "^2.5.1" - lodash "^4.17.10" - source-map "^0.5.0" - trim-right "^1.0.1" - -"@babel/generator@^7.2.2": +"@babel/generator@^7.0.0", "@babel/generator@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.2.tgz#18c816c70962640eab42fe8cae5f3947a5c65ccc" integrity sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg== @@ -182,17 +171,7 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz#5fc31de522ec0ef0899dc9b3e7cf6a5dd655f362" - integrity sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.0.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-replace-supers@^7.2.3": +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5" integrity sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA== @@ -245,12 +224,7 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.2.tgz#85c5c47af6d244fab77bce6b9bd830e38c978409" - integrity sha512-x5HFsW+E/nQalGMw7hu+fvPqnBeBaIr0lWJ2SG0PPL2j+Pm9lYvCrsZJGIgauPIENx0v10INIyFjmSNUD/gSqQ== - -"@babel/parser@^7.2.2", "@babel/parser@^7.2.3": +"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.3.tgz#32f5df65744b70888d17872ec106b02434ba1489" integrity sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA== @@ -617,16 +591,7 @@ js-levenshtein "^1.1.3" semver "^5.3.0" -"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" - integrity sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.1.2" - "@babel/types" "^7.1.2" - -"@babel/template@^7.2.2": +"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== @@ -635,22 +600,7 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.1.0.tgz#503ec6669387efd182c3888c4eec07bcc45d91b2" - integrity sha512-bwgln0FsMoxm3pLOgrrnGaXk18sSM9JNf1/nHC/FksmNGFbYnPWY4GYCfLxyP1KRmfsxqkRpfoa6xr6VuuSxdw== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.0.0" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - debug "^3.1.0" - globals "^11.1.0" - lodash "^4.17.10" - -"@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== @@ -665,16 +615,7 @@ globals "^11.1.0" lodash "^4.17.10" -"@babel/types@^7.0.0", "@babel/types@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.2.tgz#183e7952cf6691628afdc2e2b90d03240bac80c0" - integrity sha512-pb1I05sZEKiSlMUV9UReaqsCPUpgbHHHu2n1piRm7JkuBkm6QxcaIzKu6FMnMtCbih/cEYTR+RGYYC96Yk9HAg== - dependencies: - esutils "^2.0.2" - lodash "^4.17.10" - to-fast-properties "^2.0.0" - -"@babel/types@^7.2.0", "@babel/types@^7.2.2": +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e" integrity sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg== @@ -1654,6 +1595,11 @@ babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26. lodash "^4.17.4" to-fast-properties "^1.0.3" +babylon@7.0.0-beta.19: + version "7.0.0-beta.19" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.19.tgz#e928c7e807e970e0536b078ab3e0c48f9e052503" + integrity sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A== + babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1746,7 +1692,7 @@ blob@0.0.4: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" integrity sha1-vPEwUspURj8w+fx+lbmkdjCpSSE= -bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.3: +bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@~3.5.0: version "3.5.3" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== @@ -2099,6 +2045,13 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +catharsis@~0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.9.tgz#98cc890ca652dd2ef0e70b37925310ff9e90fc8b" + integrity sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is= + dependencies: + underscore-contrib "~0.3.0" + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2329,7 +2282,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0: +commander@2, commander@^2.10.0, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -3246,6 +3199,11 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +docdash@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/docdash/-/docdash-1.0.2.tgz#0449a8f6bb247f563020b78a5485dea95ae2e094" + integrity sha512-IEM57bWPLtVXpUeCKbiGvHsHtW9O9ZiiBPfeQDAZ7JdQiAF3aNWQoJ3e/+uJ63lHO009yn0tbJjtKpXJ2AURCQ== + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -3554,7 +3512,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -4639,7 +4597,7 @@ got@^8.0.3: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.9: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== @@ -6128,11 +6086,41 @@ js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0: argparse "^1.0.7" esprima "^4.0.0" +js2xmlparser@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-3.0.0.tgz#3fb60eaa089c5440f9319f51760ccd07e2499733" + integrity sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM= + dependencies: + xmlcreate "^1.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= +jsdoc-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-vue/-/jsdoc-vue-1.0.0.tgz#ff3ac1ba6bc4a74079bb79058a7bf0066e346235" + integrity sha1-/zrBumvEp0B5u3kFinvwBm40YjU= + +jsdoc@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.5.5.tgz#484521b126e81904d632ff83ec9aaa096708fa4d" + integrity sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg== + dependencies: + babylon "7.0.0-beta.19" + bluebird "~3.5.0" + catharsis "~0.8.9" + escape-string-regexp "~1.0.5" + js2xmlparser "~3.0.0" + klaw "~2.0.0" + marked "~0.3.6" + mkdirp "~0.5.1" + requizzle "~0.2.1" + strip-json-comments "~2.0.1" + taffydb "2.6.2" + underscore "~1.8.3" + jsdom@^11.5.1: version "11.12.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" @@ -6342,12 +6330,12 @@ karma@^3.0.0: tmp "0.0.33" useragent "2.2.1" -katex@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.9.0.tgz#26a7d082c21d53725422d2d71da9b2d8455fbd4a" - integrity sha512-lp3x90LT1tDZBW2tjLheJ98wmRMRjUHwk4QpaswT9bhqoQZ+XA4cPcjcQBxgOQNwaOSt6ZeL/a6GKQ1of3LFxQ== +katex@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.10.0.tgz#da562e5d0d5cc3aa602e27af8a9b8710bfbce765" + integrity sha512-/WRvx+L1eVBrLwX7QzKU1dQuaGnE7E8hDvx3VWfZh9HbMiCfsKWJNnYZ0S8ZMDAfAyDSofdyXIrH/hujF1fYXg== dependencies: - match-at "^0.1.1" + commander "^2.16.0" keyv@3.0.0: version "3.0.0" @@ -6385,6 +6373,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== +klaw@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-2.0.0.tgz#59c128e0dc5ce410201151194eeb9cbf858650f6" + integrity sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY= + dependencies: + graceful-fs "^4.1.9" + kleur@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" @@ -6672,15 +6667,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^0.3.12: - version "0.3.12" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.12.tgz#7cf25ff2252632f3fe2406bde258e94eee927519" - integrity sha512-k4NaW+vS7ytQn6MgJn3fYpQt20/mOgYM5Ft9BYMfQJDz2QT6yEeS9XJ8k2Nw8JTeWK/znPPW2n3UJGzyYEiMoA== - -match-at@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.1.tgz#25d040d291777704d5e6556bbb79230ec2de0540" - integrity sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q== +marked@^0.3.12, marked@~0.3.6: + version "0.3.19" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" + integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg== math-random@^1.0.1: version "1.0.1" @@ -6916,7 +6906,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: +mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -7879,10 +7869,10 @@ prettier@1.13.7: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281" integrity sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w== -prettier@1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e" - integrity sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug== +prettier@1.15.3: + version "1.15.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a" + integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg== pretty-format@^23.6.0: version "23.6.0" @@ -8441,6 +8431,13 @@ requires-port@1.x.x, requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +requizzle@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.1.tgz#6943c3530c4d9a7e46f1cddd51c158fc670cdbde" + integrity sha1-aUPDUwxNmn5G8c3dUcFY/GcM294= + dependencies: + underscore "~1.6.0" + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -9329,6 +9326,11 @@ table@^5.0.2: slice-ansi "2.0.0" string-width "^2.1.1" +taffydb@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" + integrity sha1-fLy2S1oUG2ou/CxdLGe04VCyomg= + tapable@^0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" @@ -9651,11 +9653,28 @@ undefsafe@^2.0.2: dependencies: debug "^2.2.0" +underscore-contrib@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7" + integrity sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc= + dependencies: + underscore "1.6.0" + +underscore@1.6.0, underscore@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + integrity sha1-izixDKze9jM3uLJOT/htRa6lKag= + underscore@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4" integrity sha512-4IV1DSSxC1QK48j9ONFK1MoIAKKkbE8i7u55w2R6IqBqbT7A/iG7aZBCR2Bi8piF0Uz+i/MG1aeqLwl/5vqF+A== +underscore@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -10353,6 +10372,11 @@ xmlbuilder@8.2.2: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" integrity sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M= +xmlcreate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-1.0.2.tgz#fa6bf762a60a413fb3dd8f4b03c5b269238d308f" + integrity sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8= + xmlhttprequest-ssl@~1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" |